cms_content_sync-3.0.x-dev/src/Entity/EntityStatus.php

src/Entity/EntityStatus.php
<?php

namespace Drupal\cms_content_sync\Entity;

use Drupal\cms_content_sync\PushIntent;
use Drupal\cms_content_sync\SyncIntent;
use Drupal\Core\Entity\ContentEntityBase;
use Drupal\Core\Entity\EntityChangedTrait;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityStorageInterface;
use Drupal\Core\Entity\EntityTypeInterface;
use Drupal\Core\Field\BaseFieldDefinition;
use Drupal\cms_content_sync\Entity\Pool;
use Drupal\cms_content_sync\Entity\Flow;
use Drupal\cms_content_sync\Plugin\Type\EntityHandlerPluginManager;
use Drupal\cms_content_sync\PullIntent;
use Drupal\cms_content_sync\SyncCoreInterface\SyncCoreFactory;
use Drupal\Core\Entity\TranslatableInterface;

/**
 * Defines the "Content Sync - Entity Status" entity type.
 *
 * @ingroup cms_content_sync_entity_status
 *
 * @ContentEntityType(
 *   id = "cms_content_sync_entity_status",
 *   label = @Translation("Content Sync - Entity Status"),
 *   base_table = "cms_content_sync_entity_status",
 *   entity_keys = {
 *     "id" = "id",
 *     "flow" = "flow",
 *     "pool" = "pool",
 *     "entity_uuid" = "entity_uuid",
 *     "entity_type" = "entity_type",
 *     "entity_type_version" = "entity_type_version",
 *     "flags" = "flags",
 *   },
 *   handlers = {
 *     "views_data" = "Drupal\views\EntityViewsData",
 *     "storage_schema" = "Drupal\cms_content_sync\EntityStatusStorageSchema",
 *   },
 * )
 */
class EntityStatus extends ContentEntityBase implements EntityStatusInterface {
  use EntityChangedTrait;

  public const FLAG_UNUSED_CLONED = 0x00000001;
  public const FLAG_DELETED = 0x00000002;
  public const FLAG_USER_ENABLED_PUSH = 0x00000004;
  public const FLAG_EDIT_OVERRIDE = 0x00000008;
  public const FLAG_IS_SOURCE_ENTITY = 0x00000010;
  public const FLAG_PUSH_ENABLED = 0x00000020;
  public const FLAG_DEPENDENCY_PUSH_ENABLED = 0x00000040;
  public const FLAG_LAST_PUSH_RESET = 0x00000080;
  public const FLAG_LAST_PULL_RESET = 0x00000100;
  public const FLAG_PUSH_FAILED = 0x00000200;
  public const FLAG_PULL_FAILED = 0x00000400;
  public const FLAG_PUSH_FAILED_SOFT = 0x00000800;
  public const FLAG_PULL_FAILED_SOFT = 0x00001000;
  public const FLAG_PUSHED_EMBEDDED = 0x00002000;
  public const FLAG_PULLED_EMBEDDED = 0x00004000;

  public const DATA_PULL_FAILURE = 'import_failure';
  public const DATA_PUSH_FAILURE = 'export_failure';
  public const DATA_ENTITY_PUSH_HASH = 'entity_push_hash';
  public const DATA_PARENT_ENTITY = 'parent_entity';
  public const DATA_TRANSLATION_SOURCE_URLS = 'translation_urls';
  public const DATA_TRANSLATIONS = 'translations';
  public const DATA_TRANSLATIONS_LAST_PULLED_VERSION_ID = 'import_version_id';
  public const DATA_TRANSLATIONS_LAST_PULL = 'last_import';
  public const DATA_TRANSLATIONS_LAST_PUSH = 'last_export';
  public const DATA_TRANSLATIONS_LAST_PUSH_TRIGGER = 'last_export_trigger';
  public const DATA_TRANSLATIONS_DELETED_AT = 'deleted_at';
  public const DATA_TRANSLATIONS_MISSING_REFERENCES = 'missing_references';

  public const FLOW_NO_FLOW = 'ERROR_STATUS_ENTITY_FLOW';

  /**
   * {@inheritdoc}
   */
  public static function preCreate(EntityStorageInterface $storage_controller, array &$values) {
    // Set Entity ID or UUID by default one or the other is not set.
    if (!isset($values['entity_type'])) {
      throw new \Exception(t('The type of the entity is required.'));
    }
    if (!isset($values['flow'])) {
      throw new \Exception(t('The flow is required.'));
    }
    if (!isset($values['pool'])) {
      throw new \Exception(t('The pool is required.'));
    }

    /**
     * @var \Drupal\Core\Entity\EntityInterface $entity
     */
    $entity = \Drupal::service('entity.repository')->loadEntityByUuid($values['entity_type'], $values['entity_uuid']);

    if (!isset($values['entity_type_version'])) {
      $values['entity_type_version'] = Flow::getEntityTypeVersion($entity->getEntityType()->id(), $entity->bundle());

      return;
    }
  }

  /**
   * Get infos for pool.
   *
   * @param string $entity_type
   *   The entity types the infos should be pulled for.
   * @param string $entity_uuid
   *   The entity uuid the infos should be pulled for.
   * @param \Drupal\cms_content_sync\Entity\Pool $pool
   *   The pool the infos should be pulled for.
   *
   * @throws \Exception
   *
   * @return EntityStatus[]
   *   The status of the entity.
   */
  public static function getInfoForPool($entity_type, $entity_uuid, Pool $pool) {
    if (!$entity_type) {
      throw new \Exception('$entity_type is required.');
    }
    if (!$entity_uuid) {
      throw new \Exception('$entity_uuid is required.');
    }
    /**
     * @var EntityStatus[] $entities
     */
    return \Drupal::entityTypeManager()
      ->getStorage('cms_content_sync_entity_status')
      ->loadByProperties([
        'entity_type' => $entity_type,
        'entity_uuid' => $entity_uuid,
        'pool' => $pool->id,
      ]);
  }

  /**
   * Get a list of all entity status entities for the given entity.
   *
   * @param string $entity_type
   *   The entity type ID.
   * @param string $entity_uuid
   *   The entity UUID.
   * @param array $filter
   *   Additional filters. Usually "flow"=>... or "pool"=>...
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   *
   * @return EntityStatus[]
   *   The list of entity status entities.
   */
  public static function getInfosForEntity($entity_type, $entity_uuid, ?array $filter = NULL) {
    if (!$entity_type) {
      throw new \Exception('$entity_type is required.');
    }
    if (!$entity_uuid) {
      throw new \Exception('$entity_uuid is required.');
    }
    $base_filter = [
      'entity_type' => $entity_type,
      'entity_uuid' => $entity_uuid,
    ];

    $filters_combined = $base_filter;
    $filter_without_flow = isset($filter['flow']) && (empty($filter['flow']) || self::FLOW_NO_FLOW == $filter['flow']);

    if ($filter_without_flow) {
      $filters_combined = array_merge($filters_combined, [
        'flow' => self::FLOW_NO_FLOW,
      ]);
    }
    elseif ($filter) {
      $filters_combined = array_merge($filters_combined, $filter);
    }

    /**
     * @var EntityStatus[] $entities
     */
    $entities = \Drupal::entityTypeManager()
      ->getStorage('cms_content_sync_entity_status')
      ->loadByProperties($filters_combined);

    $result = [];

    // If a pull fails, we may create a status entity without a flow assigned.
    // We ignore them for normal functionality, so they're filtered out.
    if ($filter_without_flow) {
      foreach ($entities as $i => $entity) {
        if (!$entity->getFlow()) {
          $result[] = $entity;
        }
      }
    }
    else {
      foreach ($entities as $i => $entity) {
        if ($entity->getFlow()) {
          $result[] = $entity;
        }
      }
    }

    return $result;
  }

  /**
   * Get a single entity status entity for the given entity.
   *
   * @param string $entity_type
   *   The entity type ID.
   * @param string $entity_uuid
   *   The entity UUID.
   * @param \Drupal\cms_content_sync\Entity\Flow|string $flow
   *   The flow the status entity belongs to.
   * @param \Drupal\cms_content_sync\Entity\Pool|string $pool
   *   The pool the status entity belongs to.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   * @throws \Exception
   *
   * @return EntityStatus|mixed
   *   The status entity.
   */
  public static function getInfoForEntity($entity_type, $entity_uuid, $flow, $pool = NULL) {
    if (!$entity_type) {
      throw new \Exception('$entity_type is required.');
    }
    if (!$entity_uuid) {
      throw new \Exception('$entity_uuid is required.');
    }

    $filter = [
      'entity_type' => $entity_type,
      'entity_uuid' => $entity_uuid,
    ];

    if ($pool) {
      $filter['pool'] = is_string($pool) ? $pool : $pool->id;
    }

    if ($flow) {
      $filter['flow'] = is_string($flow) ? $flow : $flow->id;
    }
    else {
      $filter['flow'] = self::FLOW_NO_FLOW;
    }

    /**
     * @var EntityStatus[] $entities
     */
    $entities = \Drupal::entityTypeManager()
      ->getStorage('cms_content_sync_entity_status')
      ->loadByProperties($filter);

    if (!$flow) {
      foreach ($entities as $entity) {
        if (!$entity->getFlow()) {
          return $entity;
        }
      }

      return NULL;
    }

    return reset($entities);
  }

  /**
   * Reset status entities.
   */
  public function resetStatus() {
    $this->setLastPush(NULL);
    $this->setLastPull(NULL);
    $this->save();

    // Above cache clearing doesn't work reliably. So we reset the whole entity cache.
    \Drupal::service('cache.entity')->deleteAll();
  }

  /**
   * Get the last push for an entity.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity to check for.
   *
   * @throws \Exception
   *
   * @return null|int
   *   Returns the last push timestamp or null if not pushed.
   */
  public static function getLastPushForEntity(EntityInterface $entity, $for_translation = FALSE) {
    $entity_status = EntityStatus::getInfosForEntity($entity->getEntityTypeId(), $entity->uuid());
    $latest = NULL;

    $language = $for_translation && $entity instanceof TranslatableInterface ? $entity->language()->getId() : NULL;

    foreach ($entity_status as $info) {
      if ($info->getLastPush($language) && (!$latest || $info->getLastPush($language) > $latest)) {
        $latest = $info->getLastPush($language);
      }
    }

    return $latest;
  }

  /**
   * Get last pull for an entity.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity to check for.
   * @param string|null $language
   *
   * @throws \Exception
   *
   * @return null|int
   *   Returns the last pull timestamp or null if not pulled.
   */
  public static function getLastPullForEntity(EntityInterface $entity, ?string $language = NULL) {
    $entity_status = EntityStatus::getInfosForEntity($entity->getEntityTypeId(), $entity->uuid());
    $latest = NULL;

    foreach ($entity_status as $info) {
      $current_last = $info->getLastPull($language);
      if ($current_last && (!$latest || $current_last > $latest)) {
        $latest = $current_last;
      }
    }

    return $latest;
  }

  /**
   * Get whether an entity was pushed or pulled before or none or both.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity to check for.
   * @param bool $for_translation
   *   Whether to only check for this specific translation.
   *
   * @throws \Exception
   *
   * @return null|string Either "push", "pull", "both" or NULL.
   */
  public static function wasEntityPushedOrPulled(EntityInterface $entity, $for_translation = FALSE) {
    $entity_status = EntityStatus::getInfosForEntity($entity->getEntityTypeId(), $entity->uuid());
    $language = $for_translation && $entity instanceof TranslatableInterface ? $entity->language()->getId() : NULL;

    $pushed = FALSE;
    $pulled = FALSE;

    foreach ($entity_status as $info) {
      if ($info->getLastPush($language)) {
        $pushed = TRUE;
      }
      if ($info->getLastPull($language)) {
        $pulled = TRUE;
      }
    }

    return $pushed ? ($pulled ? "both" : "push") : ($pulled ? "pull" : NULL);
  }

  /**
   * Access the temporary push to pool info for a give field.
   *
   * @param mixed $entity_type
   *   The entity type of the entity to check for.
   * @param mixed $uuid
   *   The uuid of the entity to check for.
   * @param mixed $field_name
   *   The name of the field to check for.
   * @param mixed $delta
   *   Delta value.
   * @param mixed $tree_position
   *   The tree position.
   * @param null|mixed $set_flow_id
   *   The flow id to be set.
   * @param null|mixed $set_pool_ids
   *   The pool ids to be set.
   * @param null|mixed $set_uuid
   *   The uuid to be set.
   */
  public static function accessTemporaryPushToPoolInfoForField($entity_type, $uuid, $field_name, $delta, $tree_position = [], $set_flow_id = NULL, $set_pool_ids = NULL, $set_uuid = NULL) {
    static $field_storage = [];

    if ($set_flow_id && $set_pool_ids) {
      $data = [
        'flow_id' => $set_flow_id,
        'pool_ids' => $set_pool_ids,
        'uuid' => $set_uuid,
      ];
      if (!isset($field_storage[$entity_type][$uuid])) {
        $field_storage[$entity_type][$uuid] = [];
      }
      $setter = &$field_storage[$entity_type][$uuid];
      foreach ($tree_position as $name) {
        if (!isset($setter[$name])) {
          $setter[$name] = [];
        }
        $setter = &$setter[$name];
      }
      if (!isset($setter[$field_name][$delta])) {
        $setter[$field_name][$delta] = [];
      }
      $setter = &$setter[$field_name][$delta];
      $setter = $data;
    }
    else {
      if (!empty($field_storage[$entity_type][$uuid])) {
        $value = $field_storage[$entity_type][$uuid];
        foreach ($tree_position as $name) {
          if (!isset($value[$name])) {
            return NULL;
          }
          $value = $value[$name];
        }

        return $value[$field_name][$delta] ?? NULL;
      }
    }

    return NULL;
  }

  /**
   * Save the selected pool for a field.
   *
   * @param \Drupal\Core\Entity\EntityInterface $parent_entity
   *   The parent entity.
   * @param string $parent_field_name
   *   The parent field name.
   * @param int $parent_field_delta
   *   The parent field delta.
   * @param \Drupal\Core\Entity\EntityInterface $reference
   *   The reference entity.
   * @param array $tree_position
   *   The tree position.
   */
  public static function saveSelectedPushToPoolForField(EntityInterface $parent_entity, $parent_field_name, $parent_field_delta, EntityInterface $reference, array $tree_position = []) {
    $data = EntityStatus::accessTemporaryPushToPoolInfoForField($parent_entity->getEntityTypeId(), $parent_entity->uuid(), $parent_field_name, $parent_field_delta, $tree_position);

    // On sites that don't push, this will be NULL.
    if (empty($data['flow_id'])) {
      return;
    }

    $values = $data['pool_ids'];

    $processed = [];
    if (is_array($values)) {
      foreach ($values as $id => $selected) {
        if ($selected && 'ignore' !== $id) {
          $processed[] = $id;
        }
      }
    }
    else {
      if ('ignore' !== $values) {
        $processed[] = $values;
      }
    }

    EntityStatus::saveSelectedPoolsToPushTo($reference, $data['flow_id'], $processed, $parent_entity, $parent_field_name);
  }

  /**
   * Save the selected pools to push to.
   *
   * @param \Drupal\Core\Entity\EntityInterface $reference
   *   The reference entity.
   * @param string $flow_id
   *   The flow id.
   * @param string[] $pool_ids
   *   The pool ids to save.
   * @param null|EntityInterface $parent_entity
   *   The parent entity.
   * @param null|string $parent_field_name
   *   The parent field name.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   * @throws \Drupal\Core\Entity\EntityStorageException
   */
  public static function saveSelectedPoolsToPushTo(EntityInterface $reference, $flow_id, array $pool_ids, ?EntityInterface $parent_entity = NULL, $parent_field_name = NULL) {
    $entity_type = $reference->getEntityTypeId();
    $bundle = $reference->bundle();
    $uuid = $reference->uuid();

    $flow = Flow::getAll()[$flow_id];
    $pools = Pool::getAll();

    $entity_type_pools = Pool::getSelectablePools($entity_type, $bundle, $parent_entity, $parent_field_name)[$flow_id]['pools'];
    foreach ($entity_type_pools as $entity_type_pool_id => $config) {
      $pool = $pools[$entity_type_pool_id];
      $entity_status = EntityStatus::getInfoForEntity($entity_type, $uuid, $flow, $pool);
      if (in_array($entity_type_pool_id, $pool_ids)) {
        if (!$entity_status) {
          $entity_status = EntityStatus::create([
            'flow' => $flow->id,
            'pool' => $pool->id,
            'entity_type' => $entity_type,
            'entity_uuid' => $uuid,
            'entity_type_version' => Flow::getEntityTypeVersion($entity_type, $bundle),
            'flags' => 0,
            'source_url' => NULL,
          ]);
        }

        $entity_status->isPushEnabled(TRUE);
        $entity_status->save();

        continue;
      }

      if ($entity_status) {
        $entity_status->isPushEnabled(FALSE);
        $entity_status->save();
      }
    }

    // Also check if the entity is going to be force pushed into another pool.
    $force_push_pools = $flow->getController()->getPoolsToPushTo($reference, PushIntent::PUSH_ANY, SyncIntent::ACTION_CREATE);
    if (count($entity_type_pools) && !count($pool_ids) && !count($force_push_pools)) {
      \Drupal::messenger()->addWarning(\Drupal::translation()->translate("You didn't assign a pool to @entity_type %entity_label so it won't be pushed along with the content.", [
        '@entity_type' => $entity_type,
        '%entity_label' => $reference->label(),
      ]));
    }
    elseif (count($entity_type_pools) && !count($pool_ids) && count($force_push_pools)) {
      $pools = '';
      $numItems = count($force_push_pools);
      $i = 0;
      if (count($force_push_pools) > 1) {
        foreach ($force_push_pools as $force_push_pool) {
          if (++$i === $numItems) {
            $pools .= $force_push_pool->label();
          }
          else {
            $pools .= $force_push_pool->label() . ', ';
          }
        }
      }
      else {
        foreach ($force_push_pools as $force_push_pool) {
          $pools = $force_push_pool->label();
        }
      }

      \Drupal::messenger()->addWarning(\Drupal::translation()->translate("You didn't assign a pool to @entity_type %entity_label, but it is going to be force pushed to the following pools based on the content sync configuration: %pools.", [
        '%pools' => $pools,
        '@entity_type' => $entity_type,
        '%entity_label' => $reference->label(),
      ]));
    }
  }

  /**
   * Get the entity this entity status belongs to.
   *
   * @return \Drupal\Core\Entity\EntityInterface
   *   The entity this entity status belongs to.
   */
  public function getEntity() {
    return \Drupal::service('entity.repository')->loadEntityByUuid(
          $this->getEntityTypeName(),
          $this->getUuid()
      );
  }

  /**
   * Returns the information if the entity has been pushed before but the last push date was reset.
   *
   * @param bool $set
   *   Optional parameter to set the value for LastPushReset.
   *
   * @return bool
   *   True if the entity has been pushed before but the last push date was reset.
   */
  public function wasLastPushReset($set = NULL) {
    if (TRUE === $set) {
      $this->set('flags', $this->get('flags')->value | self::FLAG_LAST_PUSH_RESET);
    }
    elseif (FALSE === $set) {
      $this->set('flags', $this->get('flags')->value & ~self::FLAG_LAST_PUSH_RESET);
    }

    return (bool) ($this->get('flags')->value & self::FLAG_LAST_PUSH_RESET);
  }

  /**
   * Was last pull reset.
   *
   * Returns the information if the entity has been pulled before but the
   * last import date was reset.
   *
   * @param bool $set
   *   Optional parameter to set the value for LastPullReset.
   *
   * @return bool
   *   True if the entity has been pullked before but the last pull date was reset.
   */
  public function wasLastPullReset($set = NULL) {
    if (TRUE === $set) {
      $this->set('flags', $this->get('flags')->value | self::FLAG_LAST_PULL_RESET);
    }
    elseif (FALSE === $set) {
      $this->set('flags', $this->get('flags')->value & ~self::FLAG_LAST_PULL_RESET);
    }

    return (bool) ($this->get('flags')->value & self::FLAG_LAST_PULL_RESET);
  }

  /**
   * Returns the information if the last push of the entity failed.
   *
   * @param bool $set
   *   Optional parameter to set the value for PushFailed.
   * @param bool $soft
   *   A soft fail- this was intended according to configuration. But the user might want to know why to debug different
   *                      expectations.
   * @param null|array $details
   *   If $set is TRUE, you can provide additional details on why the push failed. Can be gotten via
   *                            ->whyDidPushFail()
   *
   * @return bool
   *   True if the last push of the entity failed.
   */
  public function didPushFail($set = NULL, $soft = FALSE, $details = NULL) {
    $flag = $soft ? self::FLAG_PUSH_FAILED_SOFT : self::FLAG_PUSH_FAILED;
    if (TRUE === $set) {
      $this->set('flags', $this->get('flags')->value | $flag);
      if (!empty($details)) {
        $this->setData(self::DATA_PUSH_FAILURE, $details);
      }
    }
    elseif (FALSE === $set) {
      $this->set('flags', $this->get('flags')->value & ~$flag);
      $this->setData(self::DATA_PUSH_FAILURE, NULL);
    }

    return (bool) ($this->get('flags')->value & $flag);
  }

  /**
   * Get the details provided to ->didPushFail( TRUE, ... ) before.
   *
   * @return null|array
   *   The information why the push failed or null if it didnt.
   */
  public function whyDidPushingFail() {
    return $this->getData(self::DATA_PUSH_FAILURE);
  }

  /**
   * Returns the information if the last pull of the entity failed.
   *
   * @param bool $set
   *   Optional parameter to set the value for PullFailed.
   * @param bool $soft
   *   A soft fail- this was intended according to configuration. But the user might want to know why to debug different
   *                      expectations.
   * @param null|array $details
   *   If $set is TRUE, you can provide additional details on why the pull failed. Can be gotten via
   *                            ->whyDidPullFail()
   *
   * @return bool
   *   True if the pull failed.
   */
  public function didPullFail($set = NULL, $soft = FALSE, $details = NULL) {
    $flag = $soft ? self::FLAG_PULL_FAILED_SOFT : self::FLAG_PULL_FAILED;
    if (TRUE === $set) {
      $this->set('flags', $this->get('flags')->value | $flag);
      if (!empty($details)) {
        $this->setData(self::DATA_PULL_FAILURE, $details);
      }
    }
    elseif (FALSE === $set) {
      $this->set('flags', $this->get('flags')->value & ~$flag);
      $this->setData(self::DATA_PULL_FAILURE, NULL);
    }

    return (bool) ($this->get('flags')->value & $flag);
  }

  /**
   * Get the details provided to ->didPullFail( TRUE, ... ) before.
   *
   * @return null|array
   *   Returns the infos why the pull failed or null if it didnt.
   */
  public function whyDidPullingFail() {
    return $this->getData(self::DATA_PULL_FAILURE);
  }

  /**
   * Is push enabled.
   *
   * Returns the information if the entity has been chosen by the user to
   * be pushed with this flow and pool.
   *
   * @param bool $set
   *   Optional parameter to set the value for PushEnabled.
   * @param bool $setDependency
   *   Optional parameter to set the value for DependencyPushEnabled.
   *
   * @return bool
   *   True if push is enabled.
   */
  public function isPushEnabled($set = NULL, $setDependency = NULL) {
    if (TRUE === $set) {
      $this->set('flags', $this->get('flags')->value | self::FLAG_PUSH_ENABLED);
    }
    elseif (FALSE === $set) {
      $this->set('flags', $this->get('flags')->value & ~self::FLAG_PUSH_ENABLED);
    }
    if (TRUE === $setDependency) {
      $this->set('flags', $this->get('flags')->value | self::FLAG_DEPENDENCY_PUSH_ENABLED);
    }
    elseif (FALSE === $setDependency) {
      $this->set('flags', $this->get('flags')->value & ~self::FLAG_DEPENDENCY_PUSH_ENABLED);
    }

    return (bool) ($this->get('flags')->value & (self::FLAG_PUSH_ENABLED | self::FLAG_DEPENDENCY_PUSH_ENABLED));
  }

  /**
   * Check if manual pushing is enabled for the pool.
   *
   * Returns the information if the entity has been chosen by the user to
   * be pushed with this flow and pool.
   *
   * @return bool
   *   True if manual push is enabled.
   */
  public function isManualPushEnabled() {
    return (bool) ($this->get('flags')->value & (self::FLAG_PUSH_ENABLED));
  }

  /**
   * Check whether the entity was pushed manually (entity was pushed AND Flow is
   * configured to push this manually).
   *
   * @param string|null $language
   *
   * @return bool
   *   True if the entity was manually pushed.
   */
  public function wasPushedManually(?string $language = NULL) {
    return !!$this->getLastPush($language) && $this->getFlow() && $this->getEntity() && $this->getFlow()->getController()->canPushEntity($this->getEntity(), PushIntent::PUSH_MANUALLY, SyncIntent::ACTION_CREATE, $this->getPool());
  }

  /**
   * Check a entity is puhsed as an dependency.
   *
   * Returns the information if the entity has been pushed with this flow and
   * pool as a dependency.
   *
   * @return bool
   *   True if pushed as dependency.
   */
  public function isPushedAsDependency() {
    return (bool) ($this->get('flags')->value & (self::FLAG_DEPENDENCY_PUSH_ENABLED));
  }

  /**
   * Returns the information if the user override the entity locally.
   *
   * @param bool $set
   *   Optional parameter to set the value for EditOverride.
   * @param bool $individual
   *
   * @return bool
   *   True if the entity is overridden locally.
   */
  public function isOverriddenLocally($set = NULL, $individual = FALSE) {
    $status = EntityStatus::getInfosForEntity($this->getEntityTypeName(), $this->getUuid());
    if (TRUE === $set) {
      if ($individual) {
        $this->set('flags', $this->get('flags')->value | self::FLAG_EDIT_OVERRIDE);
      }
      else {
        foreach ($status as $info) {
          $info->isOverriddenLocally(TRUE, TRUE);
        }
      }

      return TRUE;
    }
    if (FALSE === $set) {
      if ($individual) {
        $this->set('flags', $this->get('flags')->value & ~self::FLAG_EDIT_OVERRIDE);
      }
      else {
        foreach ($status as $info) {
          $info->isOverriddenLocally(FALSE, TRUE);
        }
      }

      return FALSE;
    }

    if ($individual) {
      return (bool) ($this->get('flags')->value & self::FLAG_EDIT_OVERRIDE);
    }

    foreach ($status as $info) {
      if ($info->isOverriddenLocally(NULL, TRUE)) {
        return TRUE;
      }
    }

    return FALSE;
  }

  /**
   * The information if the entity has originally been created on this site.
   *
   * @param bool $set
   *   Optional parameter to set the value for IsSourceEntity.
   * @param mixed $individual
   *
   * @return bool
   *   True if the entity has been created on this site.
   */
  public function isSourceEntity($set = NULL, $individual = FALSE) {
    $status = EntityStatus::getInfosForEntity($this->getEntityTypeName(), $this->getUuid());
    if (TRUE === $set) {
      if ($individual) {
        $this->set('flags', $this->get('flags')->value | self::FLAG_IS_SOURCE_ENTITY);
      }
      else {
        foreach ($status as $info) {
          $info->isSourceEntity(TRUE, TRUE);
        }
        $this->isSourceEntity(TRUE, TRUE);
      }

      return TRUE;
    }
    if (FALSE === $set) {
      if ($individual) {
        $this->set('flags', $this->get('flags')->value & ~self::FLAG_IS_SOURCE_ENTITY);
      }
      else {
        foreach ($status as $info) {
          $info->isSourceEntity(FALSE, TRUE);
        }
        $this->isSourceEntity(FALSE, TRUE);
      }

      return FALSE;
    }

    if ($individual) {
      return (bool) ($this->get('flags')->value & self::FLAG_IS_SOURCE_ENTITY);
    }

    foreach ($status as $info) {
      if ($info->isSourceEntity(NULL, TRUE)) {
        return TRUE;
      }
    }

    return $this->isSourceEntity(NULL, TRUE);
  }

  /**
   * Returns the information if the user allowed the push.
   *
   * @param bool $set
   *   Optional parameter to set the value for UserEnabledPush.
   *
   * @return bool
   *   True if the user allowed the push.
   */
  public function didUserEnablePush($set = NULL) {
    if (TRUE === $set) {
      $this->set('flags', $this->get('flags')->value | self::FLAG_USER_ENABLED_PUSH);
    }
    elseif (FALSE === $set) {
      $this->set('flags', $this->get('flags')->value & ~self::FLAG_USER_ENABLED_PUSH);
    }

    return (bool) ($this->get('flags')->value & self::FLAG_USER_ENABLED_PUSH);
  }

  /**
   * Returns the information if the entity is deleted.
   *
   * @param bool $set
   *   Optional parameter to set the value for Deleted.
   *
   * @return bool
   *   True if the entity is deleted.
   */
  public function isDeleted($set = NULL) {
    if (TRUE === $set) {
      $this->set('flags', $this->get('flags')->value | self::FLAG_DELETED);
    }
    elseif (FALSE === $set) {
      $this->set('flags', $this->get('flags')->value & ~self::FLAG_DELETED);
    }

    return (bool) ($this->get('flags')->value & self::FLAG_DELETED);
  }

  /**
   * Returns whether the entity was pushed embedded into another parent entity.
   *
   * This is always done for field collections but can also be enabled for other
   * entities like paragraphs or media entities. This can save a lot of requests
   * when entities aren't all syndicated individually.
   *
   * @param bool $set
   *   Optional parameter to set the value for the flag.
   *
   * @return bool
   *   True if the entity was pushed embedded into another parent entity.
   */
  public function wasPushedEmbedded($set = NULL) {
    if (TRUE === $set) {
      $this->set('flags', $this->get('flags')->value | self::FLAG_PUSHED_EMBEDDED);
    }
    elseif (FALSE === $set) {
      $this->set('flags', $this->get('flags')->value & ~self::FLAG_PUSHED_EMBEDDED);
    }

    return (bool) ($this->get('flags')->value & self::FLAG_PUSHED_EMBEDDED);
  }

  /**
   * Returns whether the entity was pulled embedded in another parent entity.
   *
   * This is always done for field collections but can also be enabled for other
   * entities like paragraphs or media entities. This can save a lot of requests
   * when entities aren't all syndicated individually.
   *
   * @param bool $set
   *   Optional parameter to set the value for the flag.
   *
   * @return bool
   *   True if the entity was pulled embedded in another parent entity.
   */
  public function wasPulledEmbedded($set = NULL) {
    if (TRUE === $set) {
      $this->set('flags', $this->get('flags')->value | self::FLAG_PULLED_EMBEDDED);
    }
    elseif (FALSE === $set) {
      $this->set('flags', $this->get('flags')->value & ~self::FLAG_PULLED_EMBEDDED);
    }

    return (bool) ($this->get('flags')->value & self::FLAG_PULLED_EMBEDDED);
  }

  /**
   * Set parent entity.
   *
   * If an entity is pushed or pulled embedded into another entity, we store
   * that parent entity here. This is required so that at a later point we can
   * still force pull and force push the embedded entity although it doesn't
   * exist individually.
   * This is also required to reset e.g. embedded paragraphs after the
   * "Overwrite content locally" checkbox is unchecked.
   *
   * @param string $type
   *   Type of the parent entity.
   * @param string $uuid
   *   UUID of the parent entity.
   */
  public function setParentEntity($type, $uuid) {
    $this->setData(self::DATA_PARENT_ENTITY, [
      'type' => $type,
      'uuid' => $uuid,
    ]);
  }

  /**
   * See above.
   *
   * @return null|\Drupal\Core\Entity\EntityInterface
   *   Returns the parent entity or null if none exists.
   */
  public function getParentEntity() {
    $parent = $this->getData(self::DATA_PARENT_ENTITY);
    if ($parent) {
      $matches = \Drupal::entityTypeManager()
        ->getStorage($parent['type'])
        ->loadByProperties([
          'uuid' => $parent['uuid'],
        ]);
      if (!count($matches)) {
        return NULL;
      }

      return reset($matches);
    }

    return NULL;
  }

  /**
   * Returns the timestamp for the last pull.
   *
   * @return int
   *   The last pull timestamp.
   */
  public function getLastPull(?string $language = NULL, $if_can_be_pulled = FALSE) {
    if ($if_can_be_pulled) {
      $flow = $this->getFlow();
      if (!$flow) {
        return NULL;
      }
      $entity = $this->getEntity();
      if (!$entity) {
        return NULL;
      }
      if (!$flow->getController()->canPullEntity($entity->getEntityTypeId(), $entity->bundle(), PullIntent::PULL_FORCED)) {
        return NULL;
      }
    }
    if ($language) {
      $last_pull = $this->getTranslationData($language, EntityStatus::DATA_TRANSLATIONS_LAST_PULL);
      if ($last_pull) {
        return $last_pull;
      }
      // Data is available per translation, just not this one. So it explicitly wasn't pulled before, meaning we return NULL.
      $any_translation_pull = $this->getTranslationData('*', EntityStatus::DATA_TRANSLATIONS_LAST_PULL);
      if ($any_translation_pull) {
        return NULL;
      }
    }

    return $this->get('last_import')->value;
  }

  /**
   * Set the last pull timestamp.
   *
   * @param int $timestamp
   *   The last pull timestamp to set.
   */
  public function setLastPull($timestamp, ?string $language = NULL) {
    if ($language) {
      $this->setTranslationData($language, EntityStatus::DATA_TRANSLATIONS_LAST_PULL, $timestamp);
    }
    // All translations were pushed, so we need to update all translation indices.
    else {
      $any_translation_pull = $this->getTranslationData('*', EntityStatus::DATA_TRANSLATIONS_LAST_PULL);
      // Unless none exists (for non-translatable entities and entities pulled before using v3.0.7)
      if ($any_translation_pull) {
        foreach ($this->getData(self::DATA_TRANSLATIONS) as $langcode => $values) {
          if (isset($values[EntityStatus::DATA_TRANSLATIONS_LAST_PULL])) {
            $this->setTranslationData($langcode, EntityStatus::DATA_TRANSLATIONS_LAST_PULL, $timestamp);
          }
        }
      }
    }

    if ($this->getLastPull() == $timestamp) {
      return;
    }

    $this->set('last_import', $timestamp);

    // As this pull was successful, we can now reset the flags for status entity resets and failed pulls.
    if (!empty($timestamp)) {
      $this->wasLastPullReset(FALSE);
      $this->didPullFail(FALSE);

      // Delete status entities without Flow assigned- they're no longer needed.
      $error_entities = EntityStatus::getInfosForEntity($this->getEntityTypeName(), $this->getUuid(), ['flow' => self::FLOW_NO_FLOW], TRUE);
      foreach ($error_entities as $entity) {
        $entity->delete();
      }
    }
    // Otherwise this entity has been reset.
    else {
      $this->wasLastPullReset(TRUE);
    }
  }

  /**
   * Returns the UUID of the entity this information belongs to.
   *
   * @return string
   *   The entity UUID.
   */
  public function getUuid() {
    return $this->get('entity_uuid')->value;
  }

  /**
   * Returns the entity type name of the entity this information belongs to.
   *
   * @return string
   *   The entity type name.
   */
  public function getEntityTypeName() {
    return $this->get('entity_type')->value;
  }

  /**
   * Returns the timestamp for the last push.
   *
   * @return int
   *   The last push timestamp.
   */
  public function getLastPush(?string $language = NULL) {
    if ($language) {
      $last_push = $this->getTranslationData($language, EntityStatus::DATA_TRANSLATIONS_LAST_PUSH);
      if ($last_push) {
        return $last_push;
      }
      // Data is available per translation, just not this one. So it explicitly wasn't pushed before, meaning we return NULL.
      $any_translation_push = $this->getTranslationData('*', EntityStatus::DATA_TRANSLATIONS_LAST_PUSH);
      if ($any_translation_push) {
        return NULL;
      }
    }

    return $this->get('last_export')->value;
  }

  /**
   * Returns the timestamp of the last push trigger, if any.
   *
   * @param string|null $language
   *
   * @return int|null
   */
  public function getLastPushTrigger(?string $language = NULL) {
    if ($language) {
      return $this->getTranslationData($language, self::DATA_TRANSLATIONS_LAST_PUSH_TRIGGER) ?? $this->getLastPush($language);
    }
    return $this->getData(self::DATA_TRANSLATIONS_LAST_PUSH_TRIGGER) ?? $this->getLastPush();
  }

  /**
   * Set the last pull timestamp.
   *
   * @param int $timestamp
   *   The push timestamp to set.
   */
  public function setLastPush($timestamp, ?string $language = NULL) {
    if ($language) {
      $this->setTranslationData($language, EntityStatus::DATA_TRANSLATIONS_LAST_PUSH, $timestamp);
    }
    // All translations were pushed, so we need to update all translation indices.
    else {
      $any_translation_push = $this->getTranslationData('*', EntityStatus::DATA_TRANSLATIONS_LAST_PUSH);
      // Unless none exists (for non-translatable entities and entities pushed before using v3.0.7)
      if ($any_translation_push) {
        foreach ($this->getData(self::DATA_TRANSLATIONS) as $langcode => $values) {
          if (isset($values[EntityStatus::DATA_TRANSLATIONS_LAST_PUSH])) {
            $this->setTranslationData($langcode, EntityStatus::DATA_TRANSLATIONS_LAST_PUSH, $timestamp);
          }
        }
      }
    }

    if ($this->getLastPush() == $timestamp) {
      return;
    }

    $this->set('last_export', $timestamp);

    // As this push was successful, we can now reset the flags for status entity resets and failed exports.
    if (!empty($timestamp)) {
      $this->wasLastPushReset(FALSE);
      $this->didPushFail(FALSE);
    }
    // Otherwise this entity has been reset.
    else {
      $this->wasLastPushReset(TRUE);
    }
  }

  /**
   * Sets the timestamp of the last push trigger, if any.
   *
   * @param int|null $timestamp
   * @param string|null $language
   *
   * @return int|null
   */
  public function setLastPushTrigger(?int $timestamp, ?string $language = NULL) {
    if ($language) {
      $this->setTranslationData($language, self::DATA_TRANSLATIONS_LAST_PUSH_TRIGGER, $timestamp);
    }
    else {
      $this->setData(self::DATA_TRANSLATIONS_LAST_PUSH_TRIGGER, $timestamp);
    }
  }

  /**
   * Sets the timestamp of when the translation was deleted.
   *
   * @param int $timestamp
   * @param string $language
   *
   * @return null
   */
  public function setTranslationDeletedAt(int $timestamp, string $language) {
    $this->setTranslationData($language, self::DATA_TRANSLATIONS_DELETED_AT, $timestamp);
  }

  /**
   * Gets the timestamp of when the translation was deleted.
   *
   * @param string $language
   *
   * @return int|null
   */
  public function getTranslationDeletedAt(string $language) {
    return $this->getTranslationData($language, self::DATA_TRANSLATIONS_DELETED_AT);
  }

  /**
   * Adds a missing reference to the list.
   *
   * @param string $language
   * @param string $field
   * @param mixed $reference
   *
   * @return null
   */
  public function addMissingReference(string $language, string $field, mixed $reference) {
    $data = $this->getTranslationData($language, self::DATA_TRANSLATIONS_MISSING_REFERENCES);
    if (!$data) {
      $data = [];
    }
    if (empty($data[$field])) {
      $data[$field] = [];
    }
    $data[$field][] = $reference;
    $this->setTranslationData($language, self::DATA_TRANSLATIONS_MISSING_REFERENCES, $data);
  }

  /**
   * Resets the missing reference list.
   *
   * @param string $language
   * @param string $field
   *
   * @return null
   */
  public function resetMissingReferences(string $language, string $field) {
    $data = $this->getTranslationData($language, self::DATA_TRANSLATIONS_MISSING_REFERENCES);
    if (!$data) {
      return;
    }
    unset($data[$field]);
    $this->setTranslationData($language, self::DATA_TRANSLATIONS_MISSING_REFERENCES, $data);
  }

  /**
   * Gets all missing references.
   *
   * @param string $language
   *
   * @return array|null
   */
  public function getMissingReferences(string $language) {
    return $this->getTranslationData($language, self::DATA_TRANSLATIONS_MISSING_REFERENCES);
  }

  /**
   * Check whether the given translation requires a push for it's deletion.
   *
   * @param string|null $language
   *
   * @return bool|string[]
   */
  public function translationDeletionRequiresPush(?string $language = NULL) {
    if (!$language) {
      $translations = $this->getData(self::DATA_TRANSLATIONS);
      if (!$translations) {
        return FALSE;
      }

      $languages = [];
      foreach ($translations as $language => $data) {
        if ($this->translationDeletionRequiresPush($language)) {
          $languages[] = $language;
        }
      }

      return count($languages) ? $languages : FALSE;
    }
    $deleted_at = $this->getTranslationData($language, self::DATA_TRANSLATIONS_DELETED_AT);
    if (!$deleted_at) {
      return FALSE;
    }
    $pushed_at = $this->getLastPush($language);
    $push_triggered_at = $this->getLastPushTrigger($language);
    return $pushed_at && $pushed_at < $deleted_at && (!$push_triggered_at || $push_triggered_at < $deleted_at);
  }

  /**
   * Get the flow.
   *
   * @return \Drupal\cms_content_sync\Entity\Flow
   *   The flow.
   */
  public function getFlow() {
    if (empty($this->get('flow')->value)) {
      return NULL;
    }

    $flows = Flow::getAll();
    if (empty($flows[$this->get('flow')->value])) {
      return NULL;
    }

    return $flows[$this->get('flow')->value];
  }

  /**
   * Get the pool.
   *
   * @return \Drupal\cms_content_sync\Entity\Pool
   *   The pool.
   */
  public function getPool() {
    return Pool::getAll()[$this->get('pool')->value];
  }

  /**
   * Returns the entity type version.
   *
   * @return string
   *   The entity type version.
   */
  public function getEntityTypeVersion() {
    return $this->get('entity_type_version')->value;
  }

  /**
   * Set the last pull timestamp.
   *
   * @param string $version
   *   The entity type version to be set.
   */
  public function setEntityTypeVersion($version) {
    $this->set('entity_type_version', $version);
  }

  /**
   * Returns the entities source url.
   *
   * @return string
   *   The entities source url.
   */
  public function getSourceUrl() {
    return $this->get('source_url')->value;
  }

  /**
   * Provide the entity's source url.
   *
   * @param string $url
   *   The source url to set.
   */
  public function setSourceUrl($url) {
    $this->set('source_url', $url);
  }

  /**
   * Get a previously saved key=>value pair.
   *
   * @param null|string|string[] $key
   *   The key to retrieve.
   *
   * @see self::setData()
   *
   * @return mixed
   *   Whatever you previously stored here or NULL if the key doesn't exist.
   */
  public function getData($key = NULL) {
    $data = $this->get('data')->getValue();
    if (empty($data)) {
      return NULL;
    }

    $storage = &$data[0];

    if (empty($key)) {
      return $data;
    }

    if (!is_array($key)) {
      $key = [$key];
    }

    foreach ($key as $index) {
      if (!isset($storage[$index])) {
        return NULL;
      }

      $storage = &$storage[$index];
    }

    return $storage;
  }

  /**
   * Helper for ->getData() for translation specific meta data.
   *
   * @param string $language
   * @param string $key
   *
   * @return mixed
   */
  public function getTranslationData(string $language, string $key) {
    if ($language === '*') {
      $data = $this->getData(EntityStatus::DATA_TRANSLATIONS);
      if ($data) {
        foreach ($data as $langcode => $value) {
          if (!empty($value[$key])) {
            return $value[$key];
          }
        }
      }
      return NULL;
    }
    return $this->getData([EntityStatus::DATA_TRANSLATIONS, $language, $key]);
  }

  /**
   * Set a key=>value pair.
   *
   * @param string|string[] $key
   *   The key to set (for hierarchical usage, provide
   *                               an array of indices.
   * @param mixed $value
   *   The value to set. Must be a valid value for Drupal's
   *                     "map" storage (so basic types that can be serialized).
   */
  public function setData($key, $value) {
    $data = $this->get('data')->getValue();
    if (!empty($data)) {
      $data = $data[0];
    }
    else {
      $data = [];
    }
    $storage = &$data;

    if (is_string($key) && NULL === $value) {
      if (isset($data[$key])) {
        unset($data[$key]);
      }
    }
    else {
      if (!is_array($key)) {
        $key = [$key];
      }

      foreach ($key as $index) {
        if (!isset($storage[$index])) {
          $storage[$index] = [];
        }
        $storage = &$storage[$index];
      }

      $storage = $value;
    }

    $this->set('data', $data);
  }

  /**
   * Helper for ->setData() for translation specific meta data.
   *
   * @param string $language
   * @param string $key
   * @param mixed $value
   *
   * @return void
   */
  public function setTranslationData(string $language, string $key, mixed $value) {
    $this->setData([EntityStatus::DATA_TRANSLATIONS, $language, $key], $value);
  }

  /**
   * {@inheritdoc}
   */
  public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
    $fields = parent::baseFieldDefinitions($entity_type);

    $fields['flow'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Flow'))
      ->setDescription(t('The flow the status entity is based on.'));

    $fields['pool'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Pool'))
      ->setDescription(t('The pool the entity is connected to.'));

    $fields['entity_uuid'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Entity UUID'))
      ->setDescription(t('The UUID of the entity that is synchronized.'))
      ->setSetting('max_length', 128);

    $fields['entity_type'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Entity type'))
      ->setDescription(t('The entity type of the entity that is synchronized.'));

    $fields['entity_type_version'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Entity type version'))
      ->setDescription(t('The version of the entity type provided by Content Sync.'))
      ->setSetting('max_length', 32);

    $fields['source_url'] = BaseFieldDefinition::create('string')
      ->setLabel(t('Source URL'))
      ->setDescription(t('The entities source URL.'))
      ->setRequired(FALSE)
      ->setSetting('max_length', 1000);

    $fields['last_export'] = BaseFieldDefinition::create('timestamp')
      ->setLabel(t('Last pushed'))
      ->setDescription(t('The last time the entity got pushed.'))
      ->setRequired(FALSE);

    $fields['last_import'] = BaseFieldDefinition::create('timestamp')
      ->setLabel(t('Last pulled'))
      ->setDescription(t('The last time the entity got pulled.'))
      ->setRequired(FALSE);

    $fields['flags'] = BaseFieldDefinition::create('integer')
      ->setLabel(t('Flags'))
      ->setDescription(t('Stores boolean information about the pushed/pulled entity.'))
      ->setSetting('unsigned', TRUE)
      ->setDefaultValue(0);

    $fields['data'] = BaseFieldDefinition::create('map')
      ->setLabel(t('Data'))
      ->setDescription(t('Stores further information about the pushed/pulled entity that can also be used by entity and field handlers.'))
      ->setRequired(FALSE);

    return $fields;
  }

  /**
   * Get the entity push hash.
   *
   * @return null|string
   *   The entity push hash.
   */
  public function getEntityPushHash() {
    return $this->getData(self::DATA_ENTITY_PUSH_HASH);
  }

  /**
   * Set the entity push hash.
   *
   * @param string $hash
   *   The hash to be set.
   */
  public function setEntityPushHash($hash) {
    $this->setData(self::DATA_ENTITY_PUSH_HASH, $hash);
  }

  /**
   * Get all known translation source URLs.
   *
   * @return null|array
   *   An array of known translation source URLs.
   */
  public function getAllTranslationSourceUrls() {
    return $this->getData(self::DATA_TRANSLATION_SOURCE_URLS);
  }

  /**
   * Get the source URL for the entity in the given language.
   *
   * @param string $language
   *   the language code to get the source URL for.
   * @param bool $return_default_if_null
   *   if TRUE, will return $this->getSourceUrl() if there's no more specific URL available for the given translation language.
   *
   * @return null|string
   *   The source URL for the entity in the given language.
   */
  public function getTranslationSourceUrl(string $language, $return_default_if_null = TRUE) {
    $url = $this->getData([self::DATA_TRANSLATION_SOURCE_URLS, $language]);
    if ($url) {
      return $url;
    }

    return $return_default_if_null ? $this->getSourceUrl() : NULL;
  }

  /**
   * Set the source URL for the entity in the given language.
   *
   * @param string $language
   *   the language code to Set the source URL for.
   * @param string $url
   *   the URL to set.
   */
  public function setTranslationSourceUrl(string $language, string $url) {
    $this->setData([self::DATA_TRANSLATION_SOURCE_URLS, $language], $url);
  }

}

Главная | Обратная связь

drupal hosting | друпал хостинг | it patrol .inc