You are here

class ContentEntityCdfNormalizer in Acquia Content Hub 8

Converts the Drupal entity object to a Acquia Content Hub CDF array.

Hierarchy

Expanded class hierarchy of ContentEntityCdfNormalizer

3 files declare their use of ContentEntityCdfNormalizer
ContentEntityNormalizerTest.php in tests/src/Unit/Normalizer/ContentEntityNormalizerTest.php
ContentHubEntityExportController.php in src/Controller/ContentHubEntityExportController.php
ContentHubExportQueueBase.php in src/Plugin/QueueWorker/ContentHubExportQueueBase.php
1 string reference to 'ContentEntityCdfNormalizer'
acquia_contenthub.services.yml in ./acquia_contenthub.services.yml
acquia_contenthub.services.yml
1 service uses ContentEntityCdfNormalizer
acquia_contenthub.normalizer.entity.acquia_contenthub_cdf in ./acquia_contenthub.services.yml
Drupal\acquia_contenthub\Normalizer\ContentEntityCdfNormalizer

File

src/Normalizer/ContentEntityCdfNormalizer.php, line 39

Namespace

Drupal\acquia_contenthub\Normalizer
View source
class ContentEntityCdfNormalizer extends NormalizerBase {
  use StringTranslationTrait;

  /**
   * The format that the Normalizer can handle.
   *
   * @var string
   */
  protected $format = 'acquia_contenthub_cdf';

  /**
   * Base url.
   *
   * @var string
   */
  protected $baseUrl;

  /**
   * The interface or class that this Normalizer supports.
   *
   * @var string
   */
  protected $supportedInterfaceOrClass = 'Drupal\\Core\\Entity\\ContentEntityInterface';

  /**
   * The Config factory.
   *
   * @var \Drupal\Core\Config\ConfigFactoryInterface
   */
  protected $config;

  /**
   * The content entity view modes normalizer.
   *
   * @var \Drupal\acquia_contenthub\Normalizer\ContentEntityViewModesExtractor
   */
  protected $contentEntityViewModesNormalizer;

  /**
   * The module handler service to create alter hooks.
   *
   * @var \Drupal\Core\Extension\ModuleHandlerInterface
   */
  protected $moduleHandler;

  /**
   * The Entity Repository.
   *
   * @var \Drupal\Core\Entity\EntityRepository
   */
  protected $entityRepository;

  /**
   * Base root path of the application.
   *
   * @var string
   */
  protected $baseRoot;

  /**
   * The Basic HTTP Kernel to make requests.
   *
   * @var \Symfony\Component\HttpKernel\HttpKernelInterface
   */
  protected $kernel;

  /**
   * The renderer.
   *
   * @var \Drupal\Core\Render\Renderer
   */
  protected $renderer;

  /**
   * The entity manager.
   *
   * @var \Drupal\acquia_contenthub\EntityManager
   */
  protected $entityManager;

  /**
   * The entity type manager.
   *
   * @var \Drupal\Core\Entity\EntityTypeManagerInterface
   */
  protected $entityTypeManager;

  /**
   * Logger.
   *
   * @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
   */
  protected $loggerFactory;

  /**
   * Language Manager.
   *
   * @var \Drupal\Core\Language\LanguageManagerInterface
   */
  protected $languageManager;

  /**
   * Translation Manager.
   *
   * @var \Drupal\content_translation\ContentTranslationManagerInterface
   */
  protected $translationManager;

  /**
   * Constructs an ContentEntityNormalizer object.
   *
   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
   *   The config factory.
   * @param \Drupal\acquia_contenthub\Normalizer\ContentEntityViewModesExtractorInterface $content_entity_view_modes_normalizer
   *   The content entity view modes normalizer.
   * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
   *   The module handler to create alter hooks.
   * @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
   *   The entity repository.
   * @param \Symfony\Component\HttpKernel\HttpKernelInterface $kernel
   *   The Kernel Interface.
   * @param \Drupal\Core\Render\RendererInterface $renderer
   *   The Renderer Interface.
   * @param \Drupal\acquia_contenthub\EntityManager $entity_manager
   *   The entity manager.
   * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
   *   The entity type manager.
   * @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
   *   The logger factory.
   * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
   *   The Language Manager.
   */
  public function __construct(ConfigFactoryInterface $config_factory, ContentEntityViewModesExtractorInterface $content_entity_view_modes_normalizer, ModuleHandlerInterface $module_handler, EntityRepositoryInterface $entity_repository, HttpKernelInterface $kernel, RendererInterface $renderer, EntityManager $entity_manager, EntityTypeManagerInterface $entity_type_manager, LoggerChannelFactoryInterface $logger_factory, LanguageManagerInterface $language_manager) {
    global $base_url;
    $this->baseUrl = $base_url;
    $this->config = $config_factory;
    $this->contentEntityViewModesNormalizer = $content_entity_view_modes_normalizer;
    $this->moduleHandler = $module_handler;
    $this->entityRepository = $entity_repository;
    $this->kernel = $kernel;
    $this->renderer = $renderer;
    $this->entityManager = $entity_manager;
    $this->entityTypeManager = $entity_type_manager;
    $this->loggerFactory = $logger_factory;
    $this->languageManager = $language_manager;

    // Setting this property only if content_translation is enabled.
    if ($this->moduleHandler
      ->moduleExists('content_translation')) {
      $this->translationManager = \Drupal::getContainer()
        ->get("content_translation.manager");
    }
  }

  /**
   * Obtains the serializer.
   */
  protected function getSerializer() {
    return \Drupal::service('serializer');
  }

  /**
   * Return the global base_root variable that is defined by Drupal.
   *
   * We set this to a function so it can be overridden in a PHPUnit test.
   *
   * @return string
   *   Return global base_root variable.
   */
  public function getBaseRoot() {
    if (isset($GLOBALS['base_root'])) {
      return $GLOBALS['base_root'];
    }
    return '';
  }

  /**
   * Normalizes an object into a set of arrays/scalars.
   *
   * @param object $entity
   *   Object to normalize. Due to the constraints of the class, we know that
   *   the object will be of the ContentEntityInterface type.
   * @param string $format
   *   The format that the normalization result will be encoded as.
   * @param array $context
   *   Context options for the normalizer.
   *
   * @return array|string|bool|int|float|null
   *   Return normalized data.
   */
  public function normalize($entity, $format = NULL, array $context = []) {

    // Exit if the class does not support normalizing to the given format.
    if (!$this
      ->supportsNormalization($entity, $format)) {
      return NULL;
    }

    // Creating a fake user account to give as context to the normalization.
    $account = new ContentHubUserSession($this->config
      ->get('acquia_contenthub.entity_config')
      ->get('user_role'));
    $context += [
      'account' => $account,
    ];

    // Checking for entity access permission to this particular account.
    $entity_access = $entity
      ->access('view', $account, TRUE);
    if (!$entity_access
      ->isAllowed() && !$entity_access
      ->isNeutral()) {
      return NULL;
    }

    // By executing the rendering here with this cache contexts, we are bubbling
    // it up to the dynamic page cache so that it varies by the query param
    // include_references. Do not remove.
    $cache = [
      '#cache' => [
        'contexts' => [
          'url.query_args:include_references',
        ],
      ],
    ];
    $this->renderer
      ->renderPlain($cache);

    // Add query params to the context.
    $current_uri = \Drupal::request()
      ->getRequestUri();
    $uri = UrlHelper::parse($current_uri);
    $context += [
      'query_params' => $uri['query'],
    ];

    // Set our required CDF properties.
    $entity_type_id = $context['entity_type'] = $entity
      ->getEntityTypeId();
    $entity_uuid = $entity
      ->uuid();
    $origin = $this->config
      ->get('acquia_contenthub.admin_settings')
      ->get('origin');

    // Allow other modules to intercept and do changes to the drupal entity
    // before it is converted to CDF format.
    $this->moduleHandler
      ->alter('acquia_contenthub_drupal_to_cdf', $entity_type_id, $entity);

    // Required Created field.
    if ($entity
      ->hasField('created') && $entity
      ->get('created')) {
      $created = date('c', $entity
        ->get('created')
        ->getValue()[0]['value']);
    }
    else {
      $created = date('c');
    }

    // Modified date in the CDF will correspond to the time when the entity was
    // modified in Content Hub, not in Drupal.
    $modified = date('c');

    // Base Root Path.
    $base_root = $this
      ->getBaseRoot();

    // Initialize Content Hub entity.
    $contenthub_entity = new ContentHubEntity();
    $contenthub_entity
      ->setUuid($entity_uuid)
      ->setType($entity_type_id)
      ->setOrigin($origin)
      ->setCreated($created)
      ->setModified($modified);
    if ($view_modes = $this->contentEntityViewModesNormalizer
      ->getRenderedViewModes($entity)) {
      $contenthub_entity
        ->setMetadata([
        'base_root' => $base_root,
        'view_modes' => $view_modes,
      ]);
    }

    // We have to iterate over the entity translations and add all the
    // translations versions.
    $languages = $entity
      ->getTranslationLanguages();
    foreach ($languages as $language) {
      $langcode = $language
        ->getId();
      $localized_entity = $entity
        ->getTranslation($langcode);

      // If content_translation is enabled, then check whether the current
      // translation revision of the content has been published.
      if (!empty($this->translationManager) && $this->translationManager
        ->isEnabled($entity_type_id, $entity
        ->bundle())) {

        /** @var \Drupal\content_translation\ContentTranslationMetadataWrapperInterface $translation_metadata */
        $translation_metadata = $this->translationManager
          ->getTranslationMetadata($localized_entity);
        if (!$translation_metadata
          ->isPublished()) {
          continue;
        }
      }
      $contenthub_entity = $this
        ->addFieldsToContentHubEntity($contenthub_entity, $localized_entity, $langcode, $context);
    }

    // Allow other modules to intercept and modify the CDF entity after it has
    // been normalized and before it is sent to Content Hub.
    $this->moduleHandler
      ->alter('acquia_contenthub_cdf_from_drupal', $contenthub_entity);

    // Create the array of normalized fields, starting with the URI.
    $normalized = [
      'entities' => [
        $contenthub_entity,
      ],
    ];

    // Add all references to it if the include_references is true.
    if (!empty($context['query_params']['include_references']) && $context['query_params']['include_references'] == 'true') {
      $referenced_entities = [];
      $referenced_entities = $this
        ->getMultilevelReferencedFields($entity, $referenced_entities, $context);

      // Adding path_alias entities to the referenced_entities.
      $path_uuid_attribute = $contenthub_entity
        ->getAttribute('path_uuid');
      if ($path_uuid_attribute) {
        foreach ($path_uuid_attribute
          ->getValues() as $values) {
          foreach ($values as $path_alias_uuid) {
            $path_alias_entity = $this->entityRepository
              ->loadEntityByUuid('path_alias', $path_alias_uuid);
            if ($this->entityManager
              ->isEligibleDependency($path_alias_entity)) {
              $referenced_entities[$path_alias_uuid] = $path_alias_entity;
            }
          }
        }
      }
      $referenced_entities = array_values($referenced_entities);
      foreach ($referenced_entities as $entity) {

        // Only proceed to add the dependency if:
        // - Entity is not a node and it is not translatable.
        // - Entity is a node, its not translatable and it is published.
        // - Entity is translatable and has at least one published translation.
        if (!$this->entityManager
          ->isPublished($entity)) {
          continue;
        }

        // Generate our URL where the isolated rendered view mode lives.
        // This is the best way to really make sure the content in Content Hub
        // and the content shown to any user is 100% the same.
        try {

          // Obtain the Entity CDF by making an hmac-signed internal request.
          $context['query_params']['include_references'] = 'false';
          $referenced_entity_list_cdf = $this
            ->normalize($entity, $format, $context);
          $referenced_entity_list_cdf = array_pop($referenced_entity_list_cdf);
          if (is_array($referenced_entity_list_cdf)) {
            foreach ($referenced_entity_list_cdf as $referenced_entity_cdf) {
              $normalized['entities'][] = $referenced_entity_cdf;
            }
          }
        } catch (\Exception $e) {

          // Do nothing, route does not exist.
        }
      }
    }
    return $normalized;
  }

  /**
   * Get fields from given entity.
   *
   * Get the fields from a given entity and add them to the given content hub
   * entity object.
   *
   * @param \Acquia\ContentHubClient\Entity $contenthub_entity
   *   The Content Hub Entity that will contain all the Drupal entity fields.
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
   *   The Drupal Entity.
   * @param string $langcode
   *   The language that we are parsing.
   * @param array $context
   *   Additional Context such as the account.
   *
   * @return \Acquia\ContentHubClient\Entity
   *   The Content Hub Entity with all the data in it.
   *
   * @throws \Drupal\acquia_contenthub\ContentHubException
   *   The Exception will be thrown if something is going awol.
   */
  protected function addFieldsToContentHubEntity(ContentHubEntity $contenthub_entity, ContentEntityInterface $entity, $langcode = 'und', array $context = []) {

    /** @var \Drupal\Core\Field\FieldItemListInterface[] $fields */
    $fields = $entity
      ->getFields();

    // Get our field mapping. This maps drupal field types to Content Hub
    // attribute types.
    $type_mapping = $this
      ->getFieldTypeMapping($entity);

    // Ignore the entity ID and revision ID.
    // Excluded comes here.
    $excluded_fields = $this
      ->excludedProperties($entity);
    foreach ($fields as $name => $field) {

      // Continue if this is an excluded field or the current user does not
      // have access to view it.
      if (in_array($field
        ->getFieldDefinition()
        ->getName(), $excluded_fields) || !$field
        ->access('view', $context['account'])) {
        continue;
      }

      // Get the plain version of the field in regular json.
      if ($name === 'metatag') {
        $serialized_field = $this
          ->getSerializer()
          ->normalize($field, 'json', $context);
      }
      else {
        $serialized_field = $field
          ->getValue();
      }
      $items = $serialized_field;

      // Given that vocabularies are configuration entities, they are not
      // supported in Content Hub. Instead we use the vocabulary machine name
      // as mechanism to syndicate and import them in the right vocabulary.
      if ($name === 'vid' && $entity
        ->getEntityTypeId() === 'taxonomy_term') {

        // Initialize vocabulary attribute if it doesn't exist yet.
        if (!$contenthub_entity
          ->getAttribute('vocabulary')) {
          $attribute = new Attribute(Attribute::TYPE_STRING);
          $attribute
            ->setValue($items[0]['target_id'], $langcode);
          $contenthub_entity
            ->setAttribute('vocabulary', $attribute);
        }
        else {
          $contenthub_entity
            ->setAttributeValue('vocabulary', $items[0]['target_id'], $langcode);
        }
        continue;
      }
      if ($entity
        ->get($name)
        ->getFieldDefinition()
        ->getType() === 'path' && version_compare(\Drupal::VERSION, '8.8.0', '>=')) {
        $storage = $this->entityTypeManager
          ->getStorage('path_alias');
        $aliases = $storage
          ->loadByProperties([
          'path' => "/{$entity->toUrl()->getInternalPath()}",
        ]);
        if ($aliases) {
          $alias_uuid_attribute = new Attribute(Attribute::TYPE_ARRAY_REFERENCE);
          $uuids = [];
          foreach ($aliases as $alias) {
            $uuids[$alias
              ->language()
              ->getId()][] = $alias
              ->uuid();
          }
          foreach ($uuids as $uuid_langcode => $values) {
            $alias_uuid_attribute
              ->setValue($values, $uuid_langcode);
          }
          $contenthub_entity
            ->setAttribute('path_uuid', $alias_uuid_attribute);
        }
      }

      // For path-aliases adding a UUID of the target entity.

      /** @var \Drupal\path_alias\Entity\PathAlias $entity */
      if ($name === 'path' && $entity
        ->getEntityTypeId() === 'path_alias') {

        // Extracting entities from route.
        $route_params = Url::fromUserInput($entity
          ->getPath())
          ->getRouteParameters();
        $target_entity_uuid_attribute = new Attribute(Attribute::TYPE_ARRAY_REFERENCE);
        $target_entity_type_attribute = new Attribute(Attribute::TYPE_STRING);
        foreach ($route_params as $entity_type_id => $entity_id) {
          if (!$this->entityTypeManager
            ->hasDefinition($entity_type_id)) {

            // Skip in case of unknown entity type.
            continue;
          }
          $entity_from_route = $this->entityTypeManager
            ->getStorage($entity_type_id)
            ->load($entity_id);
          if ($entity
            ->getPath() !== "/{$entity_from_route->toUrl()->getInternalPath()}") {

            // Skip mismatched entities.
            continue;
          }
          $target_entity_uuid_attribute
            ->setValue([
            $entity_from_route
              ->uuid(),
          ], $langcode);
          $target_entity_type_attribute
            ->setValue($entity_type_id, $langcode);
        }
        $contenthub_entity
          ->setAttribute('title', (new Attribute(Attribute::TYPE_STRING))
          ->setValue($entity
          ->getAlias(), $langcode));
        $contenthub_entity
          ->setAttribute('path_uuid', $target_entity_uuid_attribute);
        $contenthub_entity
          ->setAttribute('target_entity_type', $target_entity_type_attribute);
      }

      // To make it work with Paragraphs, we are converting the field
      // 'parent_id' to 'parent_uuid' because Content Hub cannot deal with
      // entity_id information.
      if ($name === 'parent_id' && $entity
        ->getEntityTypeId() === 'paragraph') {
        $attribute = new Attribute(Attribute::TYPE_STRING);
        $parent_id = $items[0]['value'];
        $parent_type = $fields['parent_type']
          ->getValue()[0]['value'];
        $parent = $this->entityTypeManager
          ->getStorage($parent_type)
          ->load($parent_id);
        $parent_uuid = $parent
          ->uuid();
        $attribute
          ->setValue($parent_uuid, $langcode);
        $contenthub_entity
          ->setAttribute('parent_uuid', $attribute);
        continue;
      }
      if ($name == 'bundle' && $entity
        ->getEntityTypeId() === 'media') {
        $attribute = new Attribute(Attribute::TYPE_ARRAY_STRING);
        $attribute
          ->setValue([
          $entity
            ->bundle(),
        ], $langcode);
        $contenthub_entity
          ->setAttribute('bundle', $attribute);
        continue;
      }

      // Try to map it to a known field type.
      $field_type = $field
        ->getFieldDefinition()
        ->getType();

      // Go to the fallback data type when the field type is not known.
      $type = $type_mapping['fallback'];
      if (isset($type_mapping[$name])) {
        $type = $type_mapping[$name];
      }
      elseif (isset($type_mapping[$field_type])) {

        // Set it to the fallback type which is string.
        $type = $type_mapping[$field_type];
      }
      if ($type == NULL) {
        continue;
      }
      $values = [];
      if ($field instanceof EntityReferenceFieldItemListInterface) {

        // Get taxonomy parent terms.
        if ($name === 'parent' && $entity
          ->getEntityTypeId() === 'taxonomy_term') {
          $storage = $this->entityTypeManager
            ->getStorage('taxonomy_term');
          $referenced_entities = $storage
            ->loadParents($entity
            ->id());
        }
        else {

          /** @var \Drupal\Core\Entity\EntityInterface[] $referenced_entities */
          $referenced_entities = $field
            ->referencedEntities();
        }
        $values[$langcode] = [];
        foreach ($referenced_entities as $key => $referenced_entity) {

          // In the case of images/files, etc... we need to add the assets.
          $file_types = [
            'image',
            'file',
            'video',
          ];
          $type_names = [
            'type',
            'bundle',
          ];

          // Special case for type as we do not want the reference for the
          // bundle. In additional to the type field a media entity has a
          // bundle field which stores a media bundle configuration entity UUID.
          if (in_array($name, $type_names, TRUE) && $referenced_entity instanceof ConfigEntityBase) {
            $values[$langcode][] = $referenced_entity
              ->id();
          }
          elseif (in_array($field_type, $file_types)) {

            // If this is a file type, then add the asset to the CDF.
            $uuid_token = '[' . $referenced_entity
              ->uuid() . ']';
            $asset_url = file_create_url($referenced_entity
              ->getFileUri());
            $asset = new Asset();
            $asset
              ->setUrl($asset_url);
            $asset
              ->setReplaceToken($uuid_token);
            $contenthub_entity
              ->addAsset($asset);

            // Now add the value.
            // Notice that we are including the "alt" and "title" attributes
            // from the file entity in the field data.
            $data = [
              'alt' => isset($items[$key]['alt']) ? $items[$key]['alt'] : '',
              'title' => isset($items[$key]['title']) ? $items[$key]['title'] : '',
              'target_uuid' => $uuid_token,
            ];
            $values[$langcode][] = json_encode($data, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT);
          }
          else {
            $values[$langcode][] = $referenced_entity
              ->uuid();
          }
        }
      }
      else {

        // If there's nothing in this field, just set it to NULL.
        if ($items == NULL) {
          $values[$langcode] = NULL;
        }
        else {

          // Only if it is a link type.
          if ($link_field = ContentHubEntityLinkFieldHandler::load($field)
            ->validate()) {
            $items = $link_field
              ->normalizeItems($items);
          }

          // Loop over the items to get the values for each field.
          foreach ($items as $item) {

            // Hotfix.
            // @todo Find a better solution for this.
            if (isset($item['_attributes'])) {
              unset($item['_attributes']);
            }
            $keys = is_array($item) ? array_keys($item) : [];
            if (count($keys) == 1 && isset($item['value'])) {
              $value = $item['value'];
            }
            else {
              if ($field instanceof PathFieldItemList) {
                $item = $field
                  ->first()
                  ->getValue();
                $item['pid'] = "";
                $item['source'] = "";
              }
              $value = json_encode($item, JSON_HEX_TAG | JSON_HEX_APOS | JSON_HEX_AMP | JSON_HEX_QUOT);
            }
            $values[$langcode][] = $value;
          }
        }
      }
      try {
        $attribute = new Attribute($type);
      } catch (\Exception $e) {
        $args['%type'] = $type;
        $message = new FormattableMarkup('No type could be registered for %type.', $args);
        throw new ContentHubException($message);
      }
      if (strstr($type, 'array')) {
        $attribute
          ->setValues($values);
      }
      else {
        $value = array_pop($values[$langcode]);
        $attribute
          ->setValue($value, $langcode);
      }

      // If attribute exists already, append to the existing values.
      if (!empty($contenthub_entity
        ->getAttribute($name))) {
        $existing_attribute = $contenthub_entity
          ->getAttribute($name);
        $this
          ->appendToAttribute($existing_attribute, $attribute
          ->getValues());
        $attribute = $existing_attribute;
      }

      // Add it to our contenthub entity.
      $contenthub_entity
        ->setAttribute($name, $attribute);
    }

    // Allow alterations of the CDF to happen.
    $context['entity'] = $entity;
    $context['langcode'] = $langcode;
    $this->moduleHandler
      ->alter('acquia_contenthub_cdf', $contenthub_entity, $context);

    // Adds the entity URL to CDF.
    $value = NULL;
    if (empty($contenthub_entity
      ->getAttribute('url'))) {
      switch ($entity
        ->getEntityTypeId()) {
        case 'file':
          $value = file_create_url($entity
            ->getFileUri());
          $filepath_attribute = new Attribute(Attribute::TYPE_STRING);
          $contenthub_entity
            ->setAttribute('_filepath', $filepath_attribute
            ->setValue($entity
            ->getFileUri()));
          break;
        default:

          // Get entity URL.
          if (!$entity
            ->isNew() && $entity
            ->hasLinkTemplate('canonical')) {
            $url = $entity
              ->toUrl();
            $url
              ->setAbsolute(TRUE);
            $value = $url
              ->toString();
          }
          break;
      }
      if (isset($value)) {
        $url_attribute = new Attribute(Attribute::TYPE_STRING);
        $contenthub_entity
          ->setAttribute('url', $url_attribute
          ->setValue($value, $langcode));
      }
    }
    return $contenthub_entity;
  }

  /**
   * Get entity reference fields.
   *
   * Get the fields from a given entity and add them to the given content hub
   * entity object.
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
   *   The Drupal Entity.
   * @param array $context
   *   Additional Context such as the account.
   *
   * @return \Drupal\Core\Entity\ContentEntityInterface[]
   *   All referenced entities.
   */
  public function getReferencedFields(ContentEntityInterface $entity, array $context = []) {

    /** @var \Drupal\acquia_contenthub\Entity\ContentHubEntityTypeConfig[] $content_hub_entity_type_ids */
    $content_hub_entity_type_ids = $this->entityManager
      ->getContentHubEntityTypeConfigurationEntities();
    $bundle_key = $this->entityTypeManager
      ->getDefinition($entity
      ->getEntityTypeId())
      ->getKey('bundle');

    // Check if 'entity_embed' exists.
    $exists_entity_embed = \Drupal::moduleHandler()
      ->moduleExists('entity_embed');
    $referenced_entities = [];

    // If it is a taxonomy term, check for parent information.
    // We are considering the parent does not change per translation.
    // https://www.drupal.org/node/2543726
    if ($entity
      ->getEntityTypeId() === 'taxonomy_term') {
      $parents = $this->entityTypeManager
        ->getStorage('taxonomy_term')
        ->loadParents($entity
        ->id());
      foreach ($parents as $parent) {
        if ($this->entityManager
          ->isEligibleDependency($parent)) {
          $referenced_entities[$parent
            ->uuid()] = $parent;
        }
      }
    }

    // Ignore the entity ID and revision ID.
    // Excluded comes here.
    $excluded_fields = $this
      ->excludedProperties($entity);

    // Find all languages for the current entity.
    $languages = $entity
      ->getTranslationLanguages();

    // Go through all the languages.
    // We have to iterate over the entity translations and add all the
    // references that are included per translation.
    foreach ($languages as $language) {
      $langcode = $language
        ->getId();
      $localized_entity = $entity
        ->getTranslation($langcode);

      /** @var \Drupal\Core\Field\FieldItemListInterface[] $fields */
      $fields = $localized_entity
        ->getFields();
      foreach ($fields as $name => $field) {

        // Continue if this is an excluded field or the current user does not
        // have access to view it.
        $context['account'] = isset($context['account']) ? $context['account'] : NULL;
        if (in_array($field
          ->getFieldDefinition()
          ->getName(), $excluded_fields) || !$field
          ->access('view', $context['account']) || $name === $bundle_key) {
          continue;
        }
        if ($exists_entity_embed) {
          $entity_embed_handler = new ContentHubEntityEmbedHandler($field);
          if ($entity_embed_handler
            ->isProcessable()) {
            $embed_entities = $entity_embed_handler
              ->getReferencedEntities();
            foreach ($embed_entities as $uuid => $embedded_entity) {
              $referenced_entities[$uuid] = $embedded_entity;
            }
          }
        }
        if ($link_field = ContentHubEntityLinkFieldHandler::load($field)
          ->validate()) {
          $link_entities = $link_field
            ->getReferencedEntities($field
            ->getValue());
          foreach ($link_entities as $link_entity) {
            $referenced_entities[$link_entity
              ->uuid()] = $link_entity;
          }
        }
        if ($field instanceof EntityReferenceFieldItemListInterface && !$field
          ->isEmpty()) {

          // Before checking each individual target entity, verify if we can
          // skip all of them at once by checking if none of the target bundles
          // are set to be exported in Content Hub configuration.
          $skip_entities = FALSE;
          $settings = $field
            ->getFieldDefinition()
            ->getSettings();
          $target_type = isset($settings['target_type']) ? $settings['target_type'] : NULL;
          if (isset($settings['handler_settings']['target_bundles'])) {
            $target_bundles = $settings['handler_settings']['target_bundles'];
          }
          else {

            // Certain field types such as file or user do not have target
            // bundles. In this case, we have to inspect each referenced entity
            // and collect all their bundles.
            $target_bundles = [];
            $field_entities = $field
              ->referencedEntities();
            foreach ($field_entities as $field_entity) {
              if ($field_entity instanceof EntityInterface) {
                $target_bundles[] = $field_entity
                  ->bundle();
              }
            }
            $target_bundles = array_unique($target_bundles);
            if (empty($target_bundles)) {

              // In cases where there is no bundle defined, the default bundle
              // name is the same as the entity type, e.g. 'file', 'user'.
              $target_bundles = [
                $target_type,
              ];
            }
          }

          // Compare the list of bundles with the bundles set to be exportable
          // in the Content Hub Entity configuration form.
          if (!empty($target_type)) {
            $skip_entities = TRUE;
            foreach ($target_bundles as $target_bundle) {

              // If there is at least one bundle set to be exportable, it means
              // this field cannot be skipped.
              if (isset($content_hub_entity_type_ids[$target_type]) && $content_hub_entity_type_ids[$target_type]
                ->isEnableIndex($target_bundle)) {
                $skip_entities = FALSE;
                break;
              }
            }
          }

          // Check each referenced entity to see if it should be exported.
          if (!$skip_entities) {
            $field_entities = $field
              ->referencedEntities();
            foreach ($field_entities as $field_entity) {
              if ($this->entityManager
                ->isEligibleDependency($field_entity)) {

                /** @var \Drupal\Core\Entity\EntityInterface[] $referenced_entities */
                $referenced_entities[$field_entity
                  ->uuid()] = $field_entity;
              }
            }
          }
        }
      }
    }
    return $referenced_entities;
  }

  /**
   * Get multilevel entity reference fields.
   *
   * Get the fields from a given entity and add them to the given content hub
   * entity object. This also includes dependencies of the dependencies.
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
   *   The Drupal Entity.
   * @param array $referenced_entities
   *   The list of Multilevel referenced entities. This must be passed as an
   *   initialized array.
   * @param array $context
   *   Additional Context such as the account.
   * @param int $depth
   *   The depth of the referenced entity (levels down from main entity).
   *
   * @return \Drupal\Core\Entity\ContentEntityInterface[]
   *   All referenced entities.
   */
  public function getMultilevelReferencedFields(ContentEntityInterface $entity, array &$referenced_entities, array $context = [], $depth = 0) {
    $depth++;
    $maximum_depth = $this->config
      ->get('acquia_contenthub.entity_config')
      ->get('dependency_depth');
    $maximum_depth = is_int($maximum_depth) ? $maximum_depth : 3;

    // Collecting all referenced_entities UUIDs.
    $uuids = array_keys($referenced_entities);

    // Obtaining all the referenced entities for the current entity.
    $ref_entities = $this
      ->getReferencedFields($entity, $context);
    foreach ($ref_entities as $uuid => $entity) {
      if (!in_array($uuid, $uuids)) {

        // @todo This if-condition is a hack to avoid Vocabulary entities.
        if ($entity instanceof ContentEntityInterface) {
          $referenced_entities[$uuid] = $entity;

          // Only search for dependencies if we are below the maximum depth
          // configured by the admin. If not set, a default of 3 will be used.
          if ($depth < $maximum_depth) {
            $this
              ->getMultilevelReferencedFields($entity, $referenced_entities, $context, $depth);
          }
        }
      }
    }
    return $referenced_entities;
  }

  /**
   * Adds Content Hub Data to Drupal Entity Fields.
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
   *   The Drupal Entity.
   * @param \Acquia\ContentHubClient\Entity $contenthub_entity
   *   The Content Hub Entity.
   * @param string $langcode
   *   The language code.
   * @param array $context
   *   Context.
   *
   * @return \Drupal\Core\Entity\ContentEntityInterface
   *   The Drupal Entity after integrating data from Content Hub.
   */
  protected function addFieldsToDrupalEntity(ContentEntityInterface $entity, ContentHubEntity $contenthub_entity, $langcode = LanguageInterface::LANGCODE_NOT_SPECIFIED, array $context = []) {

    /** @var \Drupal\Core\Field\FieldItemListInterface[] $fields */
    $fields = $entity
      ->getFields();

    // Ignore the entity ID and revision ID.
    // Excluded comes here.
    $excluded_fields = $this
      ->excludedProperties($entity);

    // Add the bundle key to be ignored as it should have already been assigned.
    $bundle_key = $this->entityTypeManager
      ->getDefinition($entity
      ->getEntityTypeId())
      ->getKey('bundle');
    $excluded_fields[] = $bundle_key;

    // We ignore `langcode` selectively because und i.e LANGCODE_NOT_SPECIFIED
    // and zxx i.e LANGCODE_NOT_APPLICABLE content requires `langcode` field
    // to *not* be excluded for such content to be importable.
    if ($entity
      ->hasTranslation($langcode)) {
      $excluded_fields[] = 'langcode';
    }

    // Iterate over all attributes.
    foreach ($contenthub_entity
      ->getAttributes() as $name => $attribute) {
      $attribute = (array) $attribute;

      // If it is an excluded property, then skip it.
      if (in_array($name, $excluded_fields)) {
        continue;
      }

      // In the case of images/files, etc... we need to add the assets.
      $file_types = [
        'image',
        'file',
        'video',
      ];
      $field = isset($fields[$name]) ? $fields[$name] : NULL;
      if (isset($field)) {

        // Try to map it to a known field type.
        $field_type = $field
          ->getFieldDefinition()
          ->getType();
        $settings = $field
          ->getFieldDefinition()
          ->getSettings();
        $value = isset($attribute['value'][$langcode]) ? $attribute['value'][$langcode] : NULL;
        $field
          ->setValue([]);
        if ($value === NULL) {
          continue;
        }
        $field
          ->setLangcode($langcode);
        if ($field instanceof EntityReferenceFieldItemListInterface) {
          $entity_type = $settings['target_type'];
          $field_item = NULL;
          foreach ($value as $item) {
            $item = json_decode($item, TRUE) ?: $item;
            if (in_array($field_type, $file_types)) {
              if (is_array($item) && isset($item['target_uuid'])) {
                $uuid = $this
                  ->removeBracketsUuid($item['target_uuid']);
                $referenced_entity = $this->entityRepository
                  ->loadEntityByUuid($entity_type, $uuid);
              }
              else {
                $uuid = $this
                  ->removeBracketsUuid($item);
                $referenced_entity = $this->entityRepository
                  ->loadEntityByUuid($entity_type, $uuid);
              }
              $field_item = $referenced_entity ? [
                'alt' => isset($item['alt']) ? $item['alt'] : ($settings['alt_field_required'] ? $referenced_entity
                  ->label() : ''),
                'title' => isset($item['title']) ? $item['title'] : ($settings['title_field_required'] ? $referenced_entity
                  ->label() : ''),
                'target_id' => $referenced_entity
                  ->id(),
              ] : NULL;
            }
            else {
              $uuid = $item;
              $referenced_entity = $this->entityRepository
                ->loadEntityByUuid($entity_type, $uuid);
              $field_item = $referenced_entity;
            }
            if ($field_item) {
              $field
                ->appendItem($field_item);
            }
          }
        }
        else {
          if ($field instanceof FieldItemListInterface && is_array($value)) {
            foreach ($value as $item) {

              // Only decode $item if it is a valid json string, otherwise just
              // assign the value as it comes.
              if (is_string($item) && isset($item[0]) && $item[0] === '{') {
                $decoded = json_decode($item, TRUE);
                if (json_last_error() === JSON_ERROR_NONE) {
                  $item = $decoded;

                  // Only do this if we are dealing with a link field type.
                  if ($link_field = ContentHubEntityLinkFieldHandler::load($field)
                    ->validate()) {
                    $item = $link_field
                      ->denormalizeItem($item);
                  }
                }
              }
              $field
                ->appendItem($item);
            }
          }
          else {
            $field
              ->setValue($value);
          }
        }
      }
    }
    return $entity;
  }

  /**
   * Append to existing values of Content Hub Attribute.
   *
   * @param \Acquia\ContentHubClient\Attribute $attribute
   *   The attribute.
   * @param array $values
   *   The attribute's values.
   */
  public function appendToAttribute(Attribute $attribute, array $values) {
    $old_values = $attribute
      ->getValues();
    $values = array_merge($old_values, $values);
    $attribute
      ->setValues($values);
  }

  /**
   * Retrieves the mapping for known data types to Content Hub's internal types.
   *
   * Inspired by the getFieldTypeMapping in search_api.
   *
   * Search API uses the complex data format to normalize the data into a
   * document-structure suitable for search engines. However, since content hub
   * for Drupal 8 just got started, it focusses on the field types for now
   * instead of on the complex data types. Changing this architecture would
   * mean that we have to adopt a very similar structure as can be seen in the
   * Utility class of Search API. That would also mean we no longer have to
   * explicitly support certain field types as they map back to the known
   * complex data types such as string, uri that are known in Drupal Core.
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
   *   An entity to map fields to.
   *
   * @return string[]
   *   An array mapping all known (and supported) Drupal field types to their
   *   corresponding Content Hub data types. Empty values mean that fields of
   *   that type should be ignored by the Content Hub.
   *
   * @see hook_acquia_contenthub_field_type_mapping_alter()
   */
  public function getFieldTypeMapping(ContentEntityInterface $entity) {

    // Getting the bundle key.
    $bundle_key = $this->entityTypeManager
      ->getDefinition($entity
      ->getEntityTypeId())
      ->getKey('bundle');
    $langcode_key = $this->entityTypeManager
      ->getDefinition($entity
      ->getEntityTypeId())
      ->getKey('langcode');
    $mapping = [];

    // It's easier to write and understand this array in the form of
    // $default_mapping => [$data_types] and flip it below.
    $default_mapping = [
      'string' => [
        // These are special field names that we do not want to parse as
        // arrays.
        'title',
        $bundle_key,
        $langcode_key,
        // This is a special field that we will want to parse as string for now.
        // @todo Replace this to work with taxonomy_vocabulary entities.
        'vid',
      ],
      'array<string>' => [
        'fallback',
        'text_with_summary',
        'image',
        'file',
        'video',
      ],
      'array<reference>' => [
        'entity_reference',
        'entity_reference_revisions',
      ],
      'array<integer>' => [
        'integer',
        'timespan',
        'timestamp',
      ],
      'array<number>' => [
        'decimal',
        'float',
      ],
      // Types we know about but want/have to ignore.
      NULL => [
        'password',
      ],
      'array<boolean>' => [
        'boolean',
      ],
    ];
    foreach ($default_mapping as $contenthub_type => $data_types) {
      foreach ($data_types as $data_type) {
        $mapping[$data_type] = $contenthub_type;
      }
    }

    // Allow other modules to intercept and define what default type they want
    // to use for their data type.
    $this->moduleHandler
      ->alter('acquia_contenthub_field_type_mapping', $mapping);
    return $mapping;
  }

  /**
   * Provides a list of entity properties that will be excluded from the CDF.
   *
   * When building the CDF entity for the Content Hub we are exporting Drupal
   * entities that will be imported by other Drupal sites, so nids, tids, fids,
   * etc. should not be transferred, as they will be different in different
   * Drupal sites. We are relying in Drupal <uuid>'s as the entity identifier.
   * So <uuid>'s will persist through the different sites.
   * (We will need to verify this claim!)
   *
   * @param \Drupal\Core\Entity\ContentEntityInterface $entity
   *   The content entity.
   *
   * @return array
   *   An array of excluded properties.
   */
  protected function excludedProperties(ContentEntityInterface $entity) {
    $excluded_fields = [
      // Globally excluded fields (for all entity types).
      'global' => [
        // The following properties are always included in constructor, so we do
        // not need to check them again.
        'created',
        'changed',
        'uri',
        'uid',
        // Getting rid of workflow fields.
        'status',
        // Do not send revisions.
        'revision_uid',
        'revision_user',
        'revision_translation_affected',
        'revision_timestamp',
        // Translation fields.
        'content_translation_uid',
        // Do not include comments.
        'comment',
        'comment_count',
        'comment_count_new',
        // Do not include moderation state.
        'moderation_state',
      ],
      // Excluded fields for nodes.
      'node' => [
        // Getting rid of workflow fields.
        'sticky',
        'promote',
      ],
      'file' => [
        'url',
      ],
    ];
    $entity_type_id = $entity
      ->getEntityTypeId();
    $entity_keys = $entity
      ->getEntityType()
      ->getKeys();

    // Ignore specific properties based on the entity type keys.
    $ignored_keys = [
      'uid',
      'id',
      'revision',
      'uuid',
    ];
    $excluded_keys = array_values(array_intersect_key($entity_keys, array_flip($ignored_keys)));

    // Provide default excluded properties per entity type.
    if (!isset($excluded_fields[$entity_type_id])) {
      $excluded_fields[$entity_type_id] = [];
    }
    $excluded = array_merge($excluded_fields['global'], $excluded_fields[$entity_type_id], $excluded_keys);
    $excluded_to_alter = [];

    // Allow users to define more excluded properties.
    // Allow other modules to intercept and define what default type they want
    // to use for their data type.
    $this->moduleHandler
      ->alter('acquia_contenthub_exclude_fields', $excluded_to_alter, $entity);
    $excluded = array_merge($excluded, $excluded_to_alter);
    return array_filter($excluded);
  }

  /**
   * Denormalizes data back into an object of the given class.
   *
   * @param mixed $data
   *   Data to restore.
   * @param string $class
   *   The expected class to instantiate.
   * @param string $format
   *   Format the given data was extracted from.
   * @param array $context
   *   Options available to the denormalizer.
   *
   * @return array
   *   Returns denormalized data.
   */
  public function denormalize($data, $class, $format = NULL, array $context = []) {
    $context += [
      'account' => NULL,
    ];

    // Exit if the class does not support denormalization of the given data,
    // class and format.
    if (!$this
      ->supportsDenormalization($data, $class, $format)) {
      return NULL;
    }
    $contenthub_entity = new ContentHubEntity($data);

    // Allow other modules to intercept and do changes to the Content Hub CDF
    // before it is denormalized to a Drupal Entity.
    $this->moduleHandler
      ->alter('acquia_contenthub_cdf_from_hub', $contenthub_entity);
    $entity_type = $contenthub_entity
      ->getType();
    $bundle_key = $this->entityTypeManager
      ->getDefinition($entity_type)
      ->getKey('bundle');
    $bundle = $contenthub_entity
      ->getAttribute($bundle_key) ? reset($contenthub_entity
      ->getAttribute($bundle_key)['value']) : NULL;
    $langcodes = !empty($contenthub_entity
      ->getAttribute('default_langcode')['value']) ? array_keys($contenthub_entity
      ->getAttribute('default_langcode')['value']) : [
      $this->languageManager
        ->getDefaultLanguage()
        ->getId(),
    ];

    // Get default langcode and remove from attributes.
    if (!empty($contenthub_entity
      ->getAttribute('default_langcode')['value'])) {
      foreach ($contenthub_entity
        ->getAttribute('default_langcode')['value'] as $key => $value) {
        if ($value[0] == TRUE) {
          $default_langcode = $key;
          continue;
        }
      }
    }
    else {
      if ($entity_type == 'file') {
        $default_langcode = key($data['attributes']['url']['value']);
        $langcodes = array_keys($data['attributes']['url']['value']);
      }
      else {
        $default_langcode = $this->languageManager
          ->getDefaultLanguage()
          ->getId();
        if ($langcode = $contenthub_entity
          ->getAttribute('langcode')) {
          $langcodes = $langcode['value'];
          if (!in_array($default_langcode, $langcodes)) {
            $default_langcode = reset($langcodes);
          }
        }
      }
    }

    // Store the translation source outside the CDF.
    $content_translation_source = $contenthub_entity
      ->getAttribute('content_translation_source');
    $contenthub_entity
      ->removeAttribute('content_translation_source');

    // Make sure those langcodes exist in the site.
    $site_langcodes = array_keys($this->languageManager
      ->getLanguages());
    if (!in_array($default_langcode, $site_langcodes, TRUE)) {
      $langcodes = array_intersect($site_langcodes, $langcodes);

      // The default language in the CDF does not exist in the site, then we
      // can use the site default's language as the entity default language and
      // if that does not exist either, then just take the first language of
      // the available ones in this site.
      $site_default_language = $this->languageManager
        ->getDefaultLanguage()
        ->getId();
      $default_langcode = in_array($site_default_language, $langcodes, TRUE) ? $site_default_language : reset($langcodes);
    }

    // Default Langcode is only used for initial entity creation. Remove now.
    $contenthub_entity
      ->removeAttribute('langcode');

    // Does this entity exist in this site already?
    $source_entity = $this->entityRepository
      ->loadEntityByUuid($entity_type, $contenthub_entity
      ->getUuid());
    $this
      ->processPassAlias($entity_type, $contenthub_entity, $langcodes);
    if ($source_entity == NULL) {

      // Transforming Content Hub Entity into a Drupal Entity.
      $values = [
        'uuid' => $contenthub_entity
          ->getUuid(),
      ];
      if ($bundle) {
        $values[$bundle_key] = $bundle;
      }

      // Set the content_translation source of whatever the default langcode
      // says.
      $values['content_translation_source'] = $content_translation_source['value'][$default_langcode][0] ?? NULL;
      if (empty($values['content_translation_source'])) {
        unset($values['content_translation_source']);

        // Set the default langcode of the parent entity.
        $values['default_langcode'] = $default_langcode == $this->languageManager
          ->getDefaultLanguage()
          ->getId();
      }
      else {
        if (!in_array($values['content_translation_source'], $langcodes, TRUE)) {
          $values['content_translation_source'] = LanguageInterface::LANGCODE_NOT_SPECIFIED;
          $contenthub_entity
            ->removeAttribute('default_langcode');
        }
        else {

          // Set the default langcode of the parent entity.
          $values['default_langcode'] = $default_langcode;
        }
      }

      // Special treatment according to entity types.
      switch ($entity_type) {
        case 'node':
          $author = $contenthub_entity
            ->getAttribute('author') ? $contenthub_entity
            ->getAttribute('author')['value'][$default_langcode] : FALSE;
          $user = Uuid::isValid($author) ? $this->entityRepository
            ->loadEntityByUuid('user', $author) : \Drupal::currentUser();
          $values['uid'] = $user
            ->id() ? $user
            ->id() : 0;

          // Set a status for the default language entity.
          $status = $contenthub_entity
            ->getAttribute('status') ? $contenthub_entity
            ->getAttribute('status')['value'][$default_langcode] : 0;
          $values['status'] = $status ? $status : 0;

          // Check if Workbench Moderation or Content Moderation modules are
          // enabled, if so, set the moderation_state to "published".
          $workbench_moderation_enabled = \Drupal::moduleHandler()
            ->moduleExists('workbench_moderation');
          $content_moderation_enabled = \Drupal::moduleHandler()
            ->moduleExists('content_moderation');
          if ($values['status'] && ($workbench_moderation_enabled || $content_moderation_enabled)) {
            $values['moderation_state'] = 'published';
          }
          break;
        case 'media':
          $attribute = $contenthub_entity
            ->getAttribute($bundle_key);
          foreach ($langcodes as $lang) {
            if (isset($attribute['value'][$lang])) {
              $value = reset($attribute['value'][$lang]);

              // Media entity didn't import by previous version of the module.
              $values[$bundle_key] = $value;
            }
          }

          // Remove an attribute to avoid the 'Error reading entity with
          // UUID="image" from Content Hub' error.
          if (!empty($values[$bundle_key])) {
            $contenthub_entity
              ->removeAttribute($bundle_key);
          }
          break;
        case 'file':

          // If this is a file, then download the asset (image) locally.
          $attribute = $contenthub_entity
            ->getAttribute('url');
          foreach ($langcodes as $lang) {
            if (isset($attribute['value'][$lang])) {
              $remote_uri = is_array($attribute['value'][$lang]) ? array_values($attribute['value'][$lang])[0] : $attribute['value'][$lang];
              $filepath = $this
                ->getFilePath($contenthub_entity);
              if ($file_drupal_path = system_retrieve_file($remote_uri, $filepath, FALSE)) {
                $values['uri'] = $file_drupal_path;
              }
              else {

                // If the file URL is not publicly accessible, then this file
                // entity cannot be created. There is no point in trying to
                // complete the creation of this entity because it will fail
                // to be saved in the system.
                // Return a NULL entity and deal with it afterwards.
                $message = $this
                  ->t('File Entity with UUID = "%uuid" cannot be created: The remote resource %uri could not be downloaded into the system. Make sure this resource has a publicly accessible URL.', [
                  '%uuid' => $values['uuid'],
                  '%uri' => $remote_uri,
                ]);
                $this->loggerFactory
                  ->get('acquia_contenthub')
                  ->error($message);
                \Drupal::messenger()
                  ->addError($message);
                return NULL;
              }
            }
          }
          break;
        case 'taxonomy_term':

          // If it is a taxonomy_term, assing the vocabulary.
          // @todo This is a hack. It should work with vocabulary entities.
          $attribute = $contenthub_entity
            ->getAttribute('vocabulary');
          foreach ($langcodes as $lang) {
            $vocabulary_machine_name = $attribute['value'][$lang];
            $vocabulary = $this
              ->getVocabularyByName($vocabulary_machine_name);
            if (isset($vocabulary)) {
              $values['vid'] = $vocabulary
                ->getOriginalId();
            }
          }
          break;
        case 'paragraph':

          // In case of paragraphs, we need to strip out the parent_uuid and
          // change it for parent_id.
          // Fix a bug happening during export where parent_uuid only includes
          // one language, which will fail during multilingual import. Extract
          // the parent UUID and replicate it into all the languages.
          $parent_uuid = current(array_filter($contenthub_entity
            ->getAttribute('parent_uuid')['value']));
          $parent_type = current(current(array_filter($contenthub_entity
            ->getAttribute('parent_type')['value'])));
          $parent_entity = $this->entityRepository
            ->loadEntityByUuid($parent_type, $parent_uuid);
          $parent_id_attribute = new Attribute(Attribute::TYPE_ARRAY_STRING);
          foreach ($langcodes as $lang) {
            $parent_id_attribute
              ->setValue([
              $parent_entity
                ->id(),
            ], $lang);
          }

          // Add parent_id attribute to entity and remove parent_uuid.
          $attributes = $contenthub_entity
            ->getAttributes();
          $attributes['parent_id'] = (array) $parent_id_attribute;
          $contenthub_entity
            ->setAttributes($attributes);
          $contenthub_entity
            ->removeAttribute('parent_uuid');
          break;
      }
      if ($contenthub_entity
        ->getAttribute('path_uuid')) {
        $contenthub_entity
          ->removeAttribute('path_uuid');
      }
      $langcode_key = $this->entityTypeManager
        ->getDefinition($entity_type)
        ->getKey('langcode');
      $values[$langcode_key] = [
        $default_langcode,
      ];
      $source_entity = $this->entityTypeManager
        ->getStorage($entity_type)
        ->create($values);
    }
    else {
      $contenthub_entity
        ->removeAttribute('default_langcode');
      $delete_translations = $this->config
        ->get('acquia_contenthub.entity_config')
        ->get('delete_mismatching_translations');
      if ($delete_translations) {

        // Make sure that if there are local translations that have been deleted
        // from Content Hub, they are deleted locally too.
        $local_languages = $source_entity
          ->getTranslationLanguages();
        $local_langcodes = array_keys($local_languages);
        $delete_translations = array_diff($local_langcodes, array_values($langcodes));
        foreach ($delete_translations as $lang) {
          if ($source_entity
            ->hasTranslation($lang)) {
            try {
              $translated = $source_entity
                ->getTranslation($lang);

              // We cannot remove default translations.
              if (!$translated
                ->isDefaultTranslation()) {
                $source_entity
                  ->removeTranslation($lang);
              }
            } catch (\Exception $e) {
              $this->loggerFactory
                ->get('acquia_contenthub')
                ->error('Cannot remove translation "@lang" for entity type = @type, id = @id, uuid = @uuid: @message', [
                '@lang' => $lang,
                '@type' => $source_entity
                  ->getEntityTypeId(),
                '@id' => $source_entity
                  ->id(),
                '@uuid' => $source_entity
                  ->uuid(),
                '@message' => $e
                  ->getMessage(),
              ]);
            }
          }
        }
      }
    }
    $entity = $source_entity;
    foreach ($langcodes as $langcode) {

      // If this language exist in incoming data but is not supported in the
      // importing site, don't import the data under this language.
      if (!$this->languageManager
        ->getLanguage($langcode)) {
        continue;
      }

      // Make sure the entity language is one of the language contained in the
      // Content Hub Entity.
      if ($source_entity
        ->hasTranslation($langcode)) {
        $localized_entity = $source_entity
          ->getTranslation($langcode);
        $entity = $this
          ->addFieldsToDrupalEntity($localized_entity, $contenthub_entity, $langcode, $context);
        continue;
      }
      if ($langcode == LanguageInterface::LANGCODE_NOT_SPECIFIED || $langcode == LanguageInterface::LANGCODE_NOT_APPLICABLE) {
        $entity = $this
          ->addFieldsToDrupalEntity($source_entity, $contenthub_entity, $langcode, $context);
        continue;
      }
      $localized_entity = $source_entity
        ->addTranslation($langcode, $source_entity
        ->toArray());
      $localized_entity->content_translation_source = $content_translation_source['value'][$langcode][0];

      // Grab status for the language.
      $status = $contenthub_entity
        ->getAttribute('status') ? $contenthub_entity
        ->getAttribute('status')['value'][$langcode] : 0;
      $localized_entity->status = $status ? $status : 0;
      $entity = $this
        ->addFieldsToDrupalEntity($localized_entity, $contenthub_entity, $langcode, $context);
    }

    // Allow other modules to intercept and do changes to the Drupal entity
    // after it has been denormalized from a Content Hub CDF.
    $this->moduleHandler
      ->alter('acquia_contenthub_drupal_from_cdf', $entity_type, $entity);
    return $entity;
  }

  /**
   * Remove brackets from the Uuid.
   *
   * @param string $uuid_with_brakets
   *   A [UUID] enclosed within brackets.
   *
   * @return mixed
   *   The UUID without brackets, FALSE otherwise.
   */
  protected function removeBracketsUuid($uuid_with_brakets) {
    preg_match('#\\[(.*)\\]#', $uuid_with_brakets, $match);
    $uuid = isset($match[1]) ? $match[1] : '';
    if (Uuid::isValid($uuid)) {
      return $uuid;
    }
    else {
      return FALSE;
    }
  }

  /**
   * Returns a vocabulary object which matches the given name.
   *
   * Will return null if no such vocabulary exists.
   *
   * @param string $vocabulary_name
   *   This is the name of the section which is required.
   *
   * @return Object
   *   This is the vocabulary object with the name or null if no such vocabulary
   *   exists.
   */
  private function getVocabularyByName($vocabulary_name) {
    $vocabs = Vocabulary::loadMultiple(NULL);
    foreach ($vocabs as $vocab_object) {

      /** @var \Drupal\taxonomy\Entity\Vocabulary $vocab_object  */
      if ($vocab_object
        ->getOriginalId() == $vocabulary_name) {
        return $vocab_object;
      }
    }
    return NULL;
  }

  /**
   * Extracts the filepath and creates directories so that files can be stored.
   *
   * @param \Acquia\ContentHubClient\Entity $contenthub_entity
   *   The Content Hub Entity.
   *
   * @return null|string
   *   The File URI if the directories can be created, NULL otherwise.
   */
  private function getFilePath(ContentHubEntity $contenthub_entity) {
    if ($contenthub_entity
      ->getType() !== 'file') {
      return NULL;
    }
    if (!($attribute_filepath = $contenthub_entity
      ->getAttribute('_filepath'))) {
      return NULL;
    }
    $uri = isset($attribute_filepath['value'][LanguageInterface::LANGCODE_NOT_SPECIFIED]) ? $attribute_filepath['value'][LanguageInterface::LANGCODE_NOT_SPECIFIED] : NULL;
    if (substr($uri, 0, 9) !== 'public://') {
      return NULL;
    }
    $file_uri = $uri;

    // Create directories.
    $path = pathinfo($file_uri);
    $filepath = $path['dirname'];
    if (!is_dir($filepath) || !is_writable($filepath)) {
      if (!\Drupal::service('file_system')
        ->prepareDirectory($filepath, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS)) {

        // Log that directory could not be created.
        $this->loggerFactory
          ->get('acquia_contenthub')
          ->error('Cannot create files subdirectory "@dir". Please check filesystem permissions.', [
          '@dir' => $filepath,
        ]);
        $file_uri = NULL;
      }
    }
    return $file_uri;
  }

  /**
   * Processes path alias entities.
   *
   * For imported entity replaces 'path' value by the local path.
   * Removes some extra attributes like: title, path_uuid, target_entity_type.
   *
   * @param string $entity_type
   *   Type of entity to process.
   * @param \Acquia\ContentHubClient\Entity $contenthub_entity
   *   Content Hub entity for processing.
   * @param array $langcodes
   *   Langcodes for processing.
   *
   * @throws \Drupal\Core\Entity\EntityMalformedException
   * @throws \Drupal\Core\Entity\EntityStorageException
   */
  private function processPassAlias(string $entity_type, ContentHubEntity $contenthub_entity, array $langcodes) {
    if ($entity_type !== 'path_alias') {
      return;
    }
    $path_uuid = $contenthub_entity
      ->getAttribute('path_uuid');
    $target_entity_type = $contenthub_entity
      ->getAttribute('target_entity_type');
    $path_attribute = new Attribute(Attribute::TYPE_ARRAY_STRING);
    foreach ($langcodes as $langcode) {
      if (empty($path_uuid['value'][$langcode])) {
        continue;
      }
      $local_target_entity = $this->entityRepository
        ->loadEntityByUuid($target_entity_type['value'][$langcode], $path_uuid['value'][$langcode]);
      if (empty($local_target_entity)) {
        continue;
      }
      $local_path = '/' . ltrim($local_target_entity
        ->toUrl()
        ->getInternalPath(), '/');
      $path_attribute
        ->setValue($local_path, $langcode);
    }

    // Replace path attribute by local path.
    $attributes = $contenthub_entity
      ->getAttributes();
    $attributes['path'] = (array) $path_attribute;
    $contenthub_entity
      ->setAttributes($attributes);
    $contenthub_entity
      ->removeAttribute('title');
    $contenthub_entity
      ->removeAttribute('path_uuid');
    $contenthub_entity
      ->removeAttribute('target_entity_type');
  }

}

Members

Namesort descending Modifiers Type Description Overrides
CacheableNormalizerInterface::SERIALIZATION_CONTEXT_CACHEABILITY constant Name of key for bubbling cacheability metadata via serialization context.
ContentEntityCdfNormalizer::$baseRoot protected property Base root path of the application.
ContentEntityCdfNormalizer::$baseUrl protected property Base url.
ContentEntityCdfNormalizer::$config protected property The Config factory.
ContentEntityCdfNormalizer::$contentEntityViewModesNormalizer protected property The content entity view modes normalizer.
ContentEntityCdfNormalizer::$entityManager protected property The entity manager.
ContentEntityCdfNormalizer::$entityRepository protected property The Entity Repository.
ContentEntityCdfNormalizer::$entityTypeManager protected property The entity type manager.
ContentEntityCdfNormalizer::$format protected property The format that the Normalizer can handle. Overrides NormalizerBase::$format
ContentEntityCdfNormalizer::$kernel protected property The Basic HTTP Kernel to make requests.
ContentEntityCdfNormalizer::$languageManager protected property Language Manager.
ContentEntityCdfNormalizer::$loggerFactory protected property Logger.
ContentEntityCdfNormalizer::$moduleHandler protected property The module handler service to create alter hooks.
ContentEntityCdfNormalizer::$renderer protected property The renderer.
ContentEntityCdfNormalizer::$supportedInterfaceOrClass protected property The interface or class that this Normalizer supports. Overrides NormalizerBase::$supportedInterfaceOrClass
ContentEntityCdfNormalizer::$translationManager protected property Translation Manager.
ContentEntityCdfNormalizer::addFieldsToContentHubEntity protected function Get fields from given entity.
ContentEntityCdfNormalizer::addFieldsToDrupalEntity protected function Adds Content Hub Data to Drupal Entity Fields.
ContentEntityCdfNormalizer::appendToAttribute public function Append to existing values of Content Hub Attribute.
ContentEntityCdfNormalizer::denormalize public function Denormalizes data back into an object of the given class.
ContentEntityCdfNormalizer::excludedProperties protected function Provides a list of entity properties that will be excluded from the CDF.
ContentEntityCdfNormalizer::getBaseRoot public function Return the global base_root variable that is defined by Drupal.
ContentEntityCdfNormalizer::getFieldTypeMapping public function Retrieves the mapping for known data types to Content Hub's internal types.
ContentEntityCdfNormalizer::getFilePath private function Extracts the filepath and creates directories so that files can be stored.
ContentEntityCdfNormalizer::getMultilevelReferencedFields public function Get multilevel entity reference fields.
ContentEntityCdfNormalizer::getReferencedFields public function Get entity reference fields.
ContentEntityCdfNormalizer::getSerializer protected function Obtains the serializer.
ContentEntityCdfNormalizer::getVocabularyByName private function Returns a vocabulary object which matches the given name.
ContentEntityCdfNormalizer::normalize public function Normalizes an object into a set of arrays/scalars.
ContentEntityCdfNormalizer::processPassAlias private function Processes path alias entities.
ContentEntityCdfNormalizer::removeBracketsUuid protected function Remove brackets from the Uuid.
ContentEntityCdfNormalizer::__construct public function Constructs an ContentEntityNormalizer object.
NormalizerBase::addCacheableDependency protected function Adds cacheability if applicable.
NormalizerBase::checkFormat protected function Checks if the provided format is supported by this normalizer. 2
NormalizerBase::supportsDenormalization public function Implements \Symfony\Component\Serializer\Normalizer\DenormalizerInterface::supportsDenormalization() 1
NormalizerBase::supportsNormalization public function Checks whether the given class is supported for normalization by this normalizer. 1
StringTranslationTrait::$stringTranslation protected property The string translation service. 1
StringTranslationTrait::formatPlural protected function Formats a string containing a count of items.
StringTranslationTrait::getNumberOfPlurals protected function Returns the number of plurals supported by a given language.
StringTranslationTrait::getStringTranslation protected function Gets the string translation service.
StringTranslationTrait::setStringTranslation public function Sets the string translation service to use. 2
StringTranslationTrait::t protected function Translates a string to the current language or to a given language.