cms_content_sync-3.0.x-dev/src/PushIntent.php

src/PushIntent.php
<?php

namespace Drupal\cms_content_sync;

use Drupal\cms_content_sync\Controller\ContentSyncSettings;
use Drupal\cms_content_sync\Controller\Embed;
use Drupal\cms_content_sync\Controller\LoggerProxy;
use Drupal\cms_content_sync\Entity\EntityStatus;
use Drupal\cms_content_sync\Entity\Flow;
use Drupal\cms_content_sync\Entity\Pool;
use Drupal\cms_content_sync\Event\AfterEntityPush;
use Drupal\cms_content_sync\Exception\SyncException;
use Drupal\cms_content_sync\Plugin\Type\EntityHandlerPluginManager;
use Drupal\cms_content_sync\SyncCoreInterface\SyncCoreFactory;
use Drupal\Core\Entity\EntityChangedInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\TranslatableInterface;
use Drupal\Core\Link;
use Drupal\crop\Entity\Crop;
use EdgeBox\SyncCore\Exception\SyncCoreException;
use EdgeBox\SyncCore\Interfaces\ISyncCore;
use EdgeBox\SyncCore\V2\SyncCore;
use function t;

/**
 * Class PushIntent.
 */
class PushIntent extends SyncIntent {
  /**
   * @var string PUSH_DISABLED
   *             Disable pushing completely for this entity type, unless forced.
   *             - used as a configuration option
   *             - not used as $action
   */
  public const PUSH_DISABLED = 'disabled';
  /**
   * @var string PUSH_AUTOMATICALLY
   *             Automatically push all entities of this entity type.
   *             - used as a configuration option
   *             - used as $action
   */
  public const PUSH_AUTOMATICALLY = 'automatically';
  /**
   * @var string PUSH_MANUALLY
   *             Push only some of these entities, chosen manually.
   *             - used as a configuration option
   *             - used as $action
   */
  public const PUSH_MANUALLY = 'manually';
  /**
   * @var string PUSH_AS_DEPENDENCY
   *             Push only some of these entities, pushed if other pushed entities
   *             use it.
   *             - used as a configuration option
   *             - used as $action
   */
  public const PUSH_AS_DEPENDENCY = 'dependency';
  /**
   * @var string PUSH_FORCED
   *             Force the entity to be pushed (as long as a handler is also selected).
   *             Can be used programmatically for custom workflows.
   *             - not used as a configuration option
   *             - used as $action
   *             - Allows MANUAL and AUTOMATICALLY but will skip optimization (e.g. push entity even if it hasn't changed)
   */
  public const PUSH_FORCED = 'forced';
  /**
   * @var string PUSH_ANY
   *             Only used as a filter to check if the Flow pushes this entity in any
   *             way.
   *             - not used as a configuration option
   *             - not used as $action
   *             - so only used to query against Flows that have *any* push setting for a given entity (type).
   */
  public const PUSH_ANY = 'any';

  /**
   * @var int CACHE_EXPIRATION How long the push operation should be cached in seconds.
   */
  public const CACHE_EXPIRATION = 60 * 60 * 24 * 30;

  /**
   * @var string PUSH_FAILED_REQUEST_FAILED
   *             The request to the Sync Core failed completely
   */
  public const PUSH_FAILED_REQUEST_FAILED = 'export_failed_request_failed';
  /**
   * @var string PUSH_FAILED_REQUEST_INVALID_STATUS_CODE
   *             The Sync Core returned a non-2xx status code
   */
  public const PUSH_FAILED_REQUEST_INVALID_STATUS_CODE = 'export_failed_invalid_status_code';
  /**
   * @var string PUSH_FAILED_DEPENDENCY_PUSH_FAILED
   *             The entity wasn't pushed because when pushing a dependency, an error was thrown
   */
  public const PUSH_FAILED_DEPENDENCY_PUSH_FAILED = 'export_failed_dependency_export_failed';
  /**
   * @var string PUSH_FAILED_INTERNAL_ERROR
   *             The entity wasn't pushed because when serializing it, an error was thrown
   */
  public const PUSH_FAILED_INTERNAL_ERROR = 'export_failed_internal_error';
  /**
   * @var string PUSH_FAILED_HANDLER_DENIED
   *             Soft fail: The push failed because the handler returned FALSE when executing the push
   */
  public const PUSH_FAILED_HANDLER_DENIED = 'export_failed_handler_denied';
  /**
   * @var string PUSH_FAILED_UNCHANGED
   *             Soft fail: The entity wasn't pushed because it didn't change since the last push
   */
  public const PUSH_FAILED_UNCHANGED = 'export_failed_unchanged';

  /**
   * @var string NO_PUSH_REASON__JUST_PULLED The entity has been pulled
   *             during this very request, so it can't be pushed again immediately
   */
  public const NO_PUSH_REASON__JUST_PULLED = 'JUST_IMPORTED';

  /**
   * @var string NO_PUSH_REASON__NEVER_PUSHED The entity has never been
   *             pushed before, so pushing the deletion doesn't make sense (it will
   *             not even exist remotely yet)
   */
  public const NO_PUSH_REASON__NEVER_PUSHED = 'NEVER_EXPORTED';

  /**
   * @var string NO_PUSH_REASON__UNCHANGED The entity hasn't changed, so the
   *             push would not do anything
   */
  public const NO_PUSH_REASON__UNCHANGED = 'UNCHANGED';

  /**
   * @var string NO_PUSH_REASON__HANDLER_IGNORES The handler for the entity
   *             refused to push this entity. These are usually handler specific
   *             configurations like "Don't push unpublished content" for nodes.
   */
  public const NO_PUSH_REASON__HANDLER_IGNORES = 'HANDLER_IGNORES';

  /**
   * @var string NO_PUSH_REASON__NO_POOL No pool was assigned, so there's no push to take place
   */
  public const NO_PUSH_REASON__NO_POOL = 'NO_POOL';

  /**
   * @var string NO_PUSH_REASON__NOT_REGISTERD The site has not been registered yet so can't talk to the Sync Core
   */
  public const NO_PUSH_REASON__NOT_REGISTERED = 'NOT_REGISTERED';

  /**
   * @var \EdgeBox\SyncCore\Interfaces\Syndication\IPushSingle
   */
  protected $operation;

  /**
   * @var \EdgeBox\SyncCore\Interfaces\Syndication\IPushMultiple
   */
  protected $asyncOperation;

  protected $isQuickEdited = FALSE;

  protected $entityVersionHash;

  /**
   * @var array
   *            A list of all pushed entities to make sure entities aren't pushed
   *            multiple times during the same request in the format
   *            [$action][$entity_type][$bundle][$uuid] => TRUE
   */
  protected static $pushed = [];

  /**
   * @var array
   *            pushed. Can be queried via self::getNoPushReason($entity). Structure:
   *            [ entity_type_id:string ][ entity_uuid:string ] => string|Exception
   */
  protected static $noPushReasons = [];
  /**
   * @var array
   *            Structure:
   *            [ entity_type_id:string ][ entity_uuid:string ] => message
   */
  protected static $noPushMessages = [];

  /**
   * @var PushIntent[]
   */
  protected $embeddedPushIntents = [];

  protected $individualLanguage;

  /**
   * @var string[]
   */
  protected $languages;

  /**
   * @var string[]
   */
  protected $deletedLanguages;

  /**
   * PushIntent constructor.
   *
   * @param $reason
   * @param $action
   * @param null|mixed $individual_language
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   * @throws \Exception
   */
  public function __construct(Flow $flow, array $pools, $reason, $action, EntityInterface $entity, $individual_language = NULL, ?array $languages = NULL, ?array $deleted_languages = NULL) {
    parent::__construct($flow, $pools, $reason, $action, $entity->getEntityTypeId(), $entity->bundle(), $entity->uuid(), EntityHandlerPluginManager::getIdOrNull($entity), NULL, (bool) $individual_language);

    $this->individualLanguage = $individual_language;
    $this->languages = self::canHandleTranslationsIndependently() ? $languages : NULL;
    $this->deletedLanguages = $deleted_languages;

    if ($entity instanceof TranslatableInterface) {
      if (!$individual_language || !$entity->hasTranslation($individual_language)) {
        $entity = $entity->getUntranslated();
      }
      elseif ($entity->language()->getId() !== $individual_language) {
        $entity = $entity->getTranslation($individual_language);
      }
    }

    if (!$this->entity_status->getLastPush($individual_language)) {
      if (!EntityStatus::getLastPullForEntity($entity) && !PullIntent::entityHasBeenPulledFromRemoteSite($entity->getEntityTypeId(), $entity->uuid())) {
        $this->entity_status->isSourceEntity(TRUE);
      }
    }

    $this->entity = $entity;

    $moduleHandler = \Drupal::service('module_handler');
    $quickedit_enabled = $moduleHandler->moduleExists('quickedit');
    if ($quickedit_enabled && !empty(\Drupal::service('tempstore.private')->get('quickedit')->get($entity->uuid()))) {
      $this->isQuickEdited = TRUE;
    }

    $type_config = $flow->getController()->getEntityTypeConfig($entity->getEntityTypeId(), $entity->bundle());

    if (empty($type_config['version'])) {
      throw new \Exception('Please export your Flow ' . $flow->id() . ' before pushing an entity of type ' . $entity->getEntityTypeId() . '.' . $entity->bundle() . '.');
    }

    $this->operation = reset($this->pools)
      ->getClient()
      ->getSyndicationService()
      ->pushSingle(
              $this->flow->id,
              $entity->getEntityTypeId(),
              $entity->bundle(),
              $type_config['version'],
              $entity->language()->getId(),
              $entity->uuid(),
              EntityHandlerPluginManager::getIdOrNull($entity)
          )
      ->asDependency(PushIntent::PUSH_AS_DEPENDENCY == $this->flow->getController()->getEntityTypeConfig($entity->getEntityTypeId(), $entity->bundle())['export']);

    foreach ($this->pools as $pool) {
      $this->operation->toPool($pool->id);
    }

    $type_config = $this->flow->getController()->getEntityTypeConfig($entity->getEntityTypeId(), $entity->bundle());
    $this->entityVersionHash = $type_config['version'];
  }

  /**
   *
   */
  public static function setNoPushMessage(string $entity_type, string $shared_entity_id, $message) {
    self::$noPushMessages[$entity_type][$shared_entity_id] = $message;
  }

  /**
   *
   */
  public static function getNoPushMessage(string $entity_type, string $shared_entity_id) {
    return self::$noPushMessages[$entity_type][$shared_entity_id] ?? NULL;
  }

  /**
   *
   */
  public function setIgnoreMessage($message) {
    self::setNoPushMessage($this->entityType, $this->getSharedId(), $message);
  }

  /**
   * Get the correct synchronization for a specific action on a given entity.
   *
   * @param string|string[] $reason
   * @param string $action
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   *
   * @return \Drupal\cms_content_sync\Entity\Flow[]
   */
  public static function getFlowsForEntity(EntityInterface $entity, $reason, $action = SyncIntent::ACTION_CREATE) {
    $flows = Flow::getAll();

    $result = [];

    foreach ($flows as $flow) {
      if ($flow->getController()->canPushEntity($entity, $reason, $action)) {
        $result[] = $flow;
      }
    }

    return $result;
  }

  /**
   * Serialize the given entity using the entity push and field push
   * handlers.
   *
   * @throws \Drupal\cms_content_sync\Exception\SyncException
   *
   * @return bool
   *   Whether or not the serialized entity could be created
   */
  public function serialize() {
    return $this->getHandler()->push($this);
  }

  /**
   * @return \EdgeBox\SyncCore\Interfaces\Syndication\IPushSingle
   */
  public function getOperation() {
    return $this->operation;
  }

  /**
   * @param \EdgeBox\SyncCore\Interfaces\Syndication\IPushMultiple $async_operation
   *
   * @return void
   */
  public function setAsyncOperation($async_operation) {
    $this->asyncOperation = $async_operation;
  }

  /**
   * The languages that were pushed or where the push has been triggered.
   *
   * @var string[]|null
   */
  protected $pushed_languages = NULL;

  /**
   *
   */
  public function executeAsync() {
    $entity = $this->getEntity();

    $handler = $this->getHandler();
    if ($handler->ignorePush($this)) {
      return FALSE;
    }

    $operation = $this->asyncOperation ?? reset($this->pools)
      ->getClient()
      ->getSyndicationService()
      ->pushMultiple($this->flow->id);

    $default_language = $entity->language()->getId();

    $item = $operation
      ->addEntity(
              $entity->getEntityTypeId(),
              $entity->bundle(),
              $this->entityVersionHash,
              $default_language,
              $entity->uuid(),
              EntityHandlerPluginManager::getIdOrNull($entity)
          )
      ->setName($entity->label())
      ->setSourceDeepLink($handler->getViewUrl($entity))
      ->isDeleted(FALSE)
      ->isSource($this->getEntityStatus()->isSourceEntity());

    $skip_unchanged = SyncCoreFactory::featureEnabled(ISyncCore::FEATURE_SKIP_UNCHANGED_TRANSLATIONS);

    $last_push = self::PUSH_FORCED != $this->getReason() ? $this->getEntityStatus()->getLastPush($this->individualLanguage) : 0;

    if ($this->languages && $entity instanceof TranslatableInterface) {
      if (!in_array($entity->language()->getId(), $this->languages)) {
        $item->hasChanged(FALSE);
      }
    }
    elseif ($skip_unchanged) {
      $changed_time = $this->getEntityChangedTime($entity, TRUE);
      if ($changed_time <= $last_push) {
        $item->hasChanged(FALSE);
      }
    }

    if ($entity instanceof TranslatableInterface && $entity->getEntityType()->getKey('langcode')) {
      $languages = array_keys($entity->getTranslationLanguages(FALSE));

      $allowed_languages = $this->flow->getController()->getAllowedLanguages();

      // Avoid spoiling to the Sync Core that new translations exist if they
      // shouldn't be pushed yet. Otherwise the Sync Core will complain that
      // this language can't be cloned to the new revision because it doesn't
      // exist at the Sync Core yet.
      if ($this->languages) {
        if (!$this->entity_status->getLastPush()) {
          $languages = $this->languages;
        }
      }

      $this->pushed_languages = [
        $default_language,
      ];
      foreach ($languages as $langcode) {
        if ($langcode === $default_language) {
          continue;
        }

        $translation_last_push = self::PUSH_FORCED != $this->getReason() ? $this->getEntityStatus()->getLastPush($langcode) : 0;

        $changed = TRUE;
        $translation = $entity->getTranslation($langcode);

        // For automated pushes don't trigger a push/exclude unpublished
        // translations if the Flow is set to "ignore unpublished" and the
        // translation was never pushed before.
        if ($this->getFlow()->getController()->getEntityTypeConfig($entity->getEntityTypeId(), $entity->bundle())['export'] === self::PUSH_AUTOMATICALLY
          && $this->getFlow()->getController()->getEntityTypeConfig($entity->getEntityTypeId(), $entity->bundle())['handler_settings']['ignore_unpublished']
          && !$translation->isPublished()
          && !$translation_last_push) {
          continue;
        }

        if ($this->languages) {
          $changed = in_array($translation->language()->getId(), $this->languages);
        }
        elseif ($skip_unchanged && $translation_last_push) {
          $changed_time = $this->getEntityChangedTime($translation, TRUE);
          if ($changed_time <= $translation_last_push) {
            $changed = FALSE;
          }
        }

        if ($allowed_languages && !in_array($langcode, $allowed_languages)) {
          $changed = FALSE;
        }

        if ($changed) {
          $this->pushed_languages[] = $langcode;
        }

        $item->addTranslation($langcode, $handler->getViewUrl($entity->getTranslation($langcode)), $changed);
      }

      if ($this->deletedLanguages) {
        foreach ($this->deletedLanguages as $deleted_language) {
          $item->deleteTranslation($deleted_language);
        }
      }
    }

    foreach ($this->pools as $pool) {
      $item->addPool($pool->id);
    }

    if ($this->asyncOperation) {
      return TRUE;
    }

    try {
      $operation->execute();
    }
    catch (SyncCoreException $e) {
      $this->handleFinishedAsyncOperation($e);

      throw new SyncException(SyncException::CODE_PUSH_REQUEST_FAILED, $e);
    }

    $this->handleFinishedAsyncOperation(NULL);

    return TRUE;
  }

  /**
   *
   */
  public function handleFinishedAsyncOperation(?SyncCoreException $e) {
    if ($e) {
      LoggerProxy::get()->error(
        'Failed to ASYNC @action entity @entity_type-@entity_bundle @entity_uuid' . PHP_EOL . '@message' . PHP_EOL . 'Got status code @status_code @reason_phrase with body:' . PHP_EOL . '@body<br>Flow: @flow_id | Pool: @pool_id',
        [
          '@action' => $this->action,
          '@entity_type' => $this->entityType,
          '@entity_bundle' => $this->bundle,
          '@entity_uuid' => $this->uuid,
          '@message' => $e->getMessage(),
          '@status_code' => $e->getStatusCode(),
          '@reason_phrase' => $e->getReasonPhrase(),
          '@body' => $e->getResponseBody() . '',
          '@flow_id' => $this->getFlow()->id(),
          '@pool_id' => implode(',', $this->getPoolIds()),
          '@ids' => (!empty($this->entity) ? ($this->entity->getEntityType()->isRevisionable() ? "entity_id:{$this->entity->id()} revision_id:{$this->entity->getRevisionId()}" : "entity_id:{$this->entity->id()}") : ('')),
        ]
      );

      $this->saveFailedPush(PushIntent::PUSH_FAILED_REQUEST_FAILED, $e->getMessage());
    }
    else {
      $entity = $this->getEntity();

      LoggerProxy::get()->info('PUSH ASYNC @action @entity_type:@bundle @uuid @ids @reason: @message<br>Flow: @flow_id | Pools: @pool_id', [
        '@reason' => $this->reason,
        '@action' => $this->action,
        '@entity_type' => $this->entityType,
        '@bundle' => $this->bundle,
        '@uuid' => $this->uuid,
        '@message' => t('The entity has been pushed asynchronously.'),
        '@flow_id' => $this->getFlow()->id(),
        '@pool_id' => implode(',', $this->getPoolIds()),
        '@ids' => (!empty($entity) ? ($entity->getEntityType()->isRevisionable() ? "entity_id:{$entity->id()} revision_id:{$entity->getRevisionId()}" : "entity_id:{$entity->id()})") : ('')),
      ]);

      $this->afterPushTriggered($this->action, $entity);
    }
  }

  protected array $cacheTags = [];

  /**
   *
   */
  public function addCacheTags(array $tags) {
    $this->cacheTags = array_merge($this->cacheTags, $tags);
  }

  /**
   *
   */
  public function getCacheTags() {
    return $this->cacheTags;
  }

  /**
   * Push the given entity.
   *
   * @param bool $return_only
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   * @throws \Drupal\cms_content_sync\Exception\SyncException
   *
   * @return bool|PushIntent TRUE|FALSE if the entity is pushed via REST.
   *   NULL|PushIntent if $return_only is set to TRUE.
   */
  public function execute($return_only = FALSE) {
    $action = $this->getAction();
    $reason = $this->getReason();
    $entity = $this->getEntity();

    // If this very request was sent to delete/create this entity, ignore the
    // push as the result of this request will already tell Sync Core it has
    // been deleted. Otherwise Sync Core will return a reasonable 404 for
    // deletions.
    if (PullIntent::entityHasBeenPulledFromRemoteSite($entity->getEntityTypeId(), $entity->uuid())) {
      self::$noPushReasons[$entity->getEntityTypeId()][$entity->uuid()] = self::NO_PUSH_REASON__JUST_PULLED;

      return FALSE;
    }

    $entity_type = $entity->getEntityTypeId();
    $entity_bundle = $entity->bundle();
    $entity_uuid = $entity->uuid();
    $entity_id = $entity->id();

    $pushed = $this->entity_status->getLastPush($this->individualLanguage);

    if ($pushed) {
      if (SyncIntent::ACTION_CREATE == $action) {
        $action = SyncIntent::ACTION_UPDATE;
      }
    }
    else {
      if (SyncIntent::ACTION_UPDATE == $action) {
        $action = SyncIntent::ACTION_CREATE;
      }
      // If the entity was deleted but has never been pushed before,
      // pushing the deletion action doesn't make sense as it doesn't even
      // exist remotely.
      elseif (SyncIntent::ACTION_DELETE == $action) {
        self::$noPushReasons[$entity->getEntityTypeId()][$entity->uuid()] = self::NO_PUSH_REASON__NEVER_PUSHED;

        return FALSE;
      }
    }

    $cms_content_sync_disable_optimization = boolval(\Drupal::config('cms_content_sync.debug')
      ->get('cms_content_sync_disable_optimization'));

    foreach ($this->pools as $pool) {
      if (isset(self::$pushed[$action][$entity_type][$entity_bundle][$entity_uuid][$pool->id]) && (!$return_only || self::$pushed[$action][$entity_type][$entity_bundle][$entity_uuid][$pool->id] instanceof PushIntent)) {
        return $return_only ? self::$pushed[$action][$entity_type][$entity_bundle][$entity_uuid][$pool->id] : (bool) self::$pushed[$action][$entity_type][$entity_bundle][$entity_uuid][$pool->id];
      }
      $alt_action = SyncIntent::ACTION_CREATE == $action ? SyncIntent::ACTION_UPDATE : (SyncIntent::ACTION_UPDATE == $action ? SyncIntent::ACTION_CREATE : NULL);
      if ($alt_action) {
        if (isset(self::$pushed[$alt_action][$entity_type][$entity_bundle][$entity_uuid][$pool->id]) && (!$return_only || self::$pushed[$alt_action][$entity_type][$entity_bundle][$entity_uuid][$pool->id] instanceof PushIntent)) {
          return $return_only ? self::$pushed[$alt_action][$entity_type][$entity_bundle][$entity_uuid][$pool->id] : (bool) self::$pushed[$alt_action][$entity_type][$entity_bundle][$entity_uuid][$pool->id];
        }
      }

      // No need to retry from this point onward.
      self::$pushed[$action][$entity_type][$entity_bundle][$entity_uuid][$pool->id] = $return_only ? $this : TRUE;
    }

    if (!$return_only && SyncIntent::ACTION_DELETE !== $action && SyncCoreFactory::featureEnabled(ISyncCore::FEATURE_PUSH_ASYNC)) {
      return $this->executeAsync();
    }

    $proceed = TRUE;

    $operation = $this->operation;

    $from_cache = FALSE;

    if (SyncIntent::ACTION_DELETE === $action) {
      $operation->setName($entity->label() ?? '(deleted)');
      $operation->delete(TRUE);
    }
    else {
      $cache_key_parts = [
        'cms_content_sync',
        'push',
        $entity_type,
        $entity_uuid,
        $this->flow->id(),
        join("|", array_column($this->pools, 'id')),
        $reason === PushIntent::PUSH_AS_DEPENDENCY ? '1' : '0',
        $this->individualLanguage ?? "",
      ];
      $cache_key = join(':', $cache_key_parts);

      $cache = \Drupal::cache();
      $cache_item = $cache->get($cache_key);
      $cache_item_data = $cache_item ? $cache_item->data : NULL;

      if (!$this->isQuickEdited && $reason === PushIntent::PUSH_AS_DEPENDENCY && $cache_item_data && isset($cache_item_data->operation) && isset($cache_item_data->proceed) && isset($cache_item_data->entityVersionHash) && $cache_item_data->entityVersionHash === $this->entityVersionHash) {
        $from_cache = TRUE;

        $proceed = $cache_item_data->proceed;
        $operation = $cache_item_data->operation;
        $this->operation = $operation;

        $this->addCacheTags($cache_item->tags);
      }
      else {
        try {
          $proceed = $this->serialize();

          if (!$cache_item_data) {
            $cache_item_data = new \stdClass();
          }

          $cache_item_data->operation = $operation;
          $cache_item_data->proceed = $proceed;
          $cache_item_data->entityVersionHash = $this->entityVersionHash;
          $this->addCacheTags($entity->getCacheTags());
          $cache->set($cache_key, $cache_item_data, time() + self::CACHE_EXPIRATION, array_unique(array_merge([SyncCoreFactory::CACHE_TAG_SYNC_CORE], $this->getCacheTags())));
        }
        catch (\Exception $e) {
          $this->saveFailedPush(PushIntent::PUSH_FAILED_INTERNAL_ERROR, $e->getMessage());

          throw new SyncException(SyncException::CODE_ENTITY_API_FAILURE, $e);
        }
      }
    }

    // If the entity didn't change, it doesn't have to be pushed again.
    // Note that we still serialize the entity above. This is required for the hash
    // of all referenced entities to be created (see PushSingle implementation).
    if (!$cms_content_sync_disable_optimization && !$this->entityChanged() && self::PUSH_FORCED != $reason
    && SyncIntent::ACTION_DELETE != $action
    && !$return_only) {
      self::$noPushReasons[$entity->getEntityTypeId()][$entity->uuid()] = self::NO_PUSH_REASON__UNCHANGED;

      return FALSE;
    }

    LoggerProxy::get()->info('@not @embed PUSH @duration @action @entity_type:@bundle @uuid (hash: @hash) @ids @reason: @message<br>Flow: @flow_id | Pool: @pool_id | Cached: @cached | Timers: @timers', [
      '@reason' => $reason,
      '@action' => $action,
      '@cached' => $from_cache ? 'YES' : 'NO',
      '@duration' => $this->formatDuration(),
      '@timers' => $this->formatTimers(),
      '@entity_type' => $entity_type,
      '@bundle' => $entity_bundle,
      '@uuid' => $entity_uuid,
      '@not' => $proceed ? '' : 'NO',
      '@embed' => $return_only ? 'EMBEDDING' : '',
      '@hash' => $this->operation->getEntityHash(),
      '@message' => $proceed ? t('The entity has been pushed.') : (self::getNoPushMessage($entity_type, $this->getSharedId()) ?? t('The entity handler denied to push this entity.')),
      '@flow_id' => $this->getFlow()->id(),
      '@pool_id' => implode(',', $this->getPoolIds()),
      '@ids' => (!empty($this->entity) ? ($this->entity->getEntityType()->isRevisionable() ? "entity_id:{$this->entity->id()} revision_id:{$this->entity->getRevisionId()}" : "entity_id:{$this->entity->id()}") : ('')),
    ]);

    // Handler chose to deliberately ignore this entity,
    // e.g. a node that wasn't published yet and is not pushed unpublished.
    if (!$proceed) {
      self::$noPushReasons[$entity->getEntityTypeId()][$entity->uuid()] = self::NO_PUSH_REASON__HANDLER_IGNORES;
      $this->saveFailedPush(PushIntent::PUSH_FAILED_HANDLER_DENIED);
      foreach ($this->pools as $pool) {
        unset(self::$pushed[$action][$entity_type][$entity_bundle][$entity_uuid][$pool->id]);
      }

      $this->extendedEntityExportLogMessage($entity);

      return $return_only ? NULL : FALSE;
    }

    // We need to update the revision timestamp as otherwise the change won't be propagated by the Sync Core.
    if ($this->isQuickEdited) {
      $revision_timestamp = $operation->getProperty('revision_timestamp');
      if (!empty($revision_timestamp[0]['value']) && $revision_timestamp[0]['value'] < $this->getRequestTime()) {
        $revision_timestamp[0]['value'] = $this->getRequestTime();
        $this->operation->setProperty('revision_timestamp', $revision_timestamp);
      }
    }

    // If the version changed, UPDATE becomes CREATE instead and DELETE requests must be performed against the old
    // version, as otherwise they would result in a 404 Not Found response.
    if ($this->entityVersionHash != $this->entity_status->getEntityTypeVersion()) {
      if (SyncIntent::ACTION_UPDATE == $action) {
        $action = SyncIntent::ACTION_CREATE;
      }
      elseif (SyncIntent::ACTION_DELETE == $action) {
        $this->entityVersionHash = $this->entity_status->getEntityTypeVersion();
      }
    }

    if ($return_only) {
      $this->extendedEntityExportLogMessage($entity);

      return $this;
    }

    try {
      $operation->execute();
    }
    catch (SyncCoreException $e) {
      LoggerProxy::get()->error(
            'Failed to @action entity @entity_type-@entity_bundle @entity_uuid @ids' . PHP_EOL . '@message' . PHP_EOL . 'Got status code @status_code @reason_phrase with body:' . PHP_EOL . '@body<br>Flow: @flow_id | Pool: @pool_id',
            [
              '@action' => $action,
              '@entity_type' => $entity_type,
              '@entity_bundle' => $entity_bundle,
              '@entity_uuid' => $entity_uuid,
              '@message' => $e->getMessage(),
              '@status_code' => $e->getStatusCode(),
              '@reason_phrase' => $e->getReasonPhrase(),
              '@body' => $e->getResponseBody() . '',
              '@flow_id' => $this->getFlow()->id(),
              '@pool_id' => implode(',', $this->getPoolIds()),
              '@ids' => (!empty($this->entity) ? ($this->entity->getEntityType()->isRevisionable() ? "entity_id:{$this->entity->id()} revision_id:{$this->entity->getRevisionId()}" : "entity_id:{$this->entity->id()}") : ('')),
            ]
            );

      $this->saveFailedPush(PushIntent::PUSH_FAILED_REQUEST_FAILED, $e->getMessage());

      throw new SyncException(SyncException::CODE_PUSH_REQUEST_FAILED, $e);
    }

    $this->afterPushTriggered($action, $entity);
    $this->afterPushExecuted($action, $entity);

    return TRUE;
  }

  /**
   * @deprecated v3.0.7
   */
  public function afterPush($action, $entity) {
    $this->afterPushTriggered($action, $entity);
    $this->afterPushExecuted($action, $entity);
  }

  /**
   *
   */
  public function afterPushTriggered($action, $entity) {
    if ($this->pushed_languages) {
      foreach ($this->pushed_languages as $language) {
        $this->entity_status->setLastPushTrigger(\Drupal::time()->getRequestTime(), $language);
      }
    }
    else {
      $this->entity_status->setLastPushTrigger(\Drupal::time()->getRequestTime());
    }

    if (!empty($this->deletedLanguages)) {
      // Deletions are marked as being successful immediately as they don't
      // require another request to the site.
      $deleted_at = time();
      foreach ($this->deletedLanguages as $language) {
        $this->entity_status->setLastPush($deleted_at, $language);
      }
    }

    // Dispatch entity push event to give other modules the possibility to react on it.
    \Drupal::service('event_dispatcher')->dispatch(new AfterEntityPush($entity, $this->pools, $this->flow, $this->reason, $this->action), AfterEntityPush::EVENT_NAME);

    $this->extendedEntityExportLogMessage($entity);

    $this->entity_status->save();
  }

  /**
   *
   */
  public function afterPushExecuted($action, $entity) {
    $this->updateEntityStatusAfterSuccessfulPush($action);
  }

  /**
   * Handle Extended Entity Export logging.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The exported entity.
   */
  public function extendedEntityExportLogMessage(EntityInterface $entity) {
    $settings = ContentSyncSettings::getInstance();
    if ($settings->getExtendedEntityExportLogging()) {
      $data = json_encode($this->operation->getData());

      \Drupal::logger('cms_content_sync_entity_export_log')->debug('%entity_type - %uuid <br>Data: <br><pre><code>%data</code></pre>', [
        '%entity_type' => $entity->getEntityTypeId(),
        '%uuid' => $entity->uuid(),
        '%data' => $data,
      ]);
    }
  }

  /**
   * @param string $action
   * @param null $parent_type
   * @param null $parent_uuid
   */
  public function updateEntityStatusAfterSuccessfulPush($action = SyncIntent::ACTION_CREATE, $parent_type = NULL, $parent_uuid = NULL) {
    static $saved = [];
    $entity = $this->getEntity();
    if (!empty($saved[$entity->getEntityTypeId()][$entity->uuid()])) {
      return;
    }
    $saved[$entity->getEntityTypeId()][$entity->uuid()] = TRUE;

    $this->entity_status->setEntityPushHash($this->operation->getEntityHash());

    if (!$this->entity_status->getLastPush($this->individualLanguage) && !$this->entity_status->getLastPull() && !empty($this->operation->getProperty('url'))) {
      $this->entity_status->setSourceUrl($this->operation->getProperty('url'));
    }

    $individual_translation = $this->isIndividualTranslation();
    if ($individual_translation && $entity instanceof TranslatableInterface) {
      if ($entity->language()->getId() !== $this->individualLanguage && $entity->hasTranslation($this->individualLanguage)) {
        $entity = $entity->getTranslation($this->individualLanguage);
      }

      $push = $this->getEntityChangedTime($entity, TRUE);
      $this->entity_status->setLastPush($push, $this->individualLanguage);
    }
    else {
      $push = $this->getEntityChangedTime($this->entity);
      $this->entity_status->setLastPush($push);
    }

    if (SyncIntent::ACTION_DELETE == $action) {
      $this->entity_status->isDeleted(TRUE);
      reset($this->pools)->markDeleted($entity->getEntityTypeId(), $entity->uuid(), $this->pools);
    }

    if ($this->entityVersionHash != $this->entity_status->getEntityTypeVersion()) {
      $this->entity_status->setEntityTypeVersion($this->entityVersionHash);
    }

    if ($parent_type && $parent_uuid) {
      $this->entity_status->wasPushedEmbedded(TRUE);
      $this->entity_status->setParentEntity($parent_type, $parent_uuid);
    }
    else {
      $this->entity_status->wasPushedEmbedded(FALSE);
    }

    $this->entity_status->save();

    foreach ($this->embeddedPushIntents as $intent) {
      $intent->updateEntityStatusAfterSuccessfulPush(SyncIntent::ACTION_CREATE, $this->entityType, $this->uuid);
    }
  }

  /**
   * Check whether the given entity is currently being pushed. Useful to check
   * against hierarchical references as for nodes and menu items for example.
   *
   * @param string $entity_type
   *   The entity type to check for.
   * @param string $uuid
   *   The UUID of the entity in question.
   * @param string $pool
   *   The pool to push to.
   * @param null|string $action
   *   See ::ACTION_*.
   *
   * @return bool
   */
  public static function isPushing(string $entity_type, string $uuid, ?string $pool = NULL, ?string $action = NULL) {
    foreach (self::$pushed as $do => $types) {
      if ($action ? $do != $action : SyncIntent::ACTION_DELETE == $do) {
        continue;
      }
      if (!isset($types[$entity_type])) {
        continue;
      }
      foreach ($types[$entity_type] as $bundle => $entities) {
        if (empty($pool)) {
          if (!empty($entities[$uuid])) {
            return TRUE;
          }
        }
        else {
          if (!empty($entities[$uuid][$pool])) {
            return TRUE;
          }
        }
      }
    }

    return FALSE;
  }

  /**
   * Get the pools to push to, filtered to not include content that was overridden
   * locally.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   * @param string $reason
   * @param string $action
   * @param \Drupal\cms_content_sync\Entity\Flow $flow
   *
   * @return \Drupal\cms_content_sync\Entity\Pool[]
   */
  public static function getPoolsToPushTo(EntityInterface $entity, string $reason, string $action, Flow $flow) {
    $all_pools = $flow->getController()->getPoolsToPushTo($entity, $reason, $action, TRUE);
    $pools = [];
    foreach ($all_pools as $pool) {
      $infos = EntityStatus::getInfosForEntity($entity->getEntityTypeId(), $entity->uuid(), ['pool' => $pool->id()]);
      $cancel = FALSE;
      foreach ($infos as $info) {
        if (!$info->getFlow()) {
          continue;
        }

        if (!$info->getLastPull()) {
          continue;
        }

        if ($info->isOverriddenLocally()) {
          $cancel = TRUE;

          break;
        }
      }

      if ($cancel) {
        continue;
      }

      $pools[] = $pool;
    }

    return $pools;
  }

  /**
   * Helper function to push an entity and throw errors if anything fails.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity to push.
   * @param string $reason
   *   {@see Flow::PUSH_*}.
   * @param string $action
   *   {@see ::ACTION_*}.
   * @param \Drupal\cms_content_sync\Entity\Flow $flow
   *   The flow to be used. If none is given, all flows that may push this
   *                                                            entity will be asked to do so for all relevant all_pools$all_pools.
   * @param \Drupal\cms_content_sync\Entity\Pool[] $all_pools
   *   The pool to be used. If not set, all relevant all_pools$all_pools for the flow will be
   *                                                            used one after another.
   * @param bool $return_intent
   *   Return the PushIntent operation instead of
   *                            executing it. Used to embed entities.
   * @param bool $individual_translation
   *   Whether to serialize only the given translation or use the root translation and provide
   *                            all available translations of it.
   * @param null|mixed $pools
   * @param string|null $individual_language
   * @param string[]|null $languages
   *
   * @throws \Drupal\cms_content_sync\Exception\SyncException
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   * @throws \Drupal\Core\Entity\EntityStorageException
   * @throws \GuzzleHttp\Exception\GuzzleException
   *
   * @return bool|PushIntent Whether the entity is configured to be pushed or not.
   *   if $return_only is given, this will return the serialized entity to embed
   *                         or NULL.
   */
  public static function pushEntity(EntityInterface $entity, $reason, $action, ?Flow $flow = NULL, $pools = NULL, $return_intent = FALSE, $individual_language = NULL, $languages = NULL) {
    if (!$flow) {
      $flows = self::getFlowsForEntity($entity, $reason, $action);
      if (!count($flows)) {
        return FALSE;
      }

      $result = FALSE;
      foreach ($flows as $flow) {
        if ($return_intent) {
          $result = self::pushEntity($entity, $reason, $action, $flow, NULL, TRUE, $individual_language, $languages);
          if ($result) {
            return $result;
          }
        }
        else {
          $result |= self::pushEntity($entity, $reason, $action, $flow, NULL, FALSE, $individual_language, $languages);
        }
      }

      return $result;
    }

    if (!$pools) {
      $pools = self::getPoolsToPushTo($entity, $reason, $action, $flow);

      if (!count($pools)) {
        return FALSE;
      }

      return self::pushEntity($entity, $reason, $action, $flow, $pools, $return_intent, $individual_language, $languages);
    }

    if (!$flow->getController()->canPushEntity($entity, $reason, $action)) {
      return FALSE;
    }

    $any_pool = reset($pools);
    // Ignore any push attempts if the site wasn't registered yet. This can
    // happen if the site imported Flow and Pool configuration but didn't
    // register.
    try {
      $client = $any_pool->getClient();

      if (!$client->isSiteRegistered()) {
        self::$noPushReasons[$entity->getEntityTypeId()][$entity->uuid()] = self::NO_PUSH_REASON__NOT_REGISTERED;

        return FALSE;
      }
    }
    catch (\Exception $e) {
      self::$noPushReasons[$entity->getEntityTypeId()][$entity->uuid()] = self::NO_PUSH_REASON__NOT_REGISTERED;

      return FALSE;
    }

    if (SyncCoreFactory::featureEnabled(ISyncCore::FEATURE_PUSH_TO_MULTIPLE_POOLS)) {
      $intent = new PushIntent($flow, $pools, $reason, $action, $entity, $individual_language, $languages);

      return $intent->execute($return_intent);
    }

    $result = FALSE;

    foreach ($pools as $pool) {
      $intent = new PushIntent($flow, [$pool], $reason, $action, $entity, $individual_language);
      if ($return_intent) {
        $result = $intent->execute(TRUE);
        if ($result) {
          return $result;
        }
      }
      else {
        $result |= $intent->execute();
      }
    }

    return $result;
  }

  /**
   * Get the reason why a push has not happened.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   * @param bool $as_message
   *
   * @return null|Exception|string see self::$noPushReasons
   */
  public static function getNoPushReason($entity, $as_message = FALSE) {
    // If push wasn't even tried, no pool has been assigned.
    if (empty(self::$noPushReasons[$entity->getEntityTypeId()][$entity->uuid()])) {
      $issue = self::NO_PUSH_REASON__NO_POOL;
    }
    else {
      $issue = self::$noPushReasons[$entity->getEntityTypeId()][$entity->uuid()];
    }

    if ($as_message) {
      $shared_id = EntityHandlerPluginManager::getSharedId($entity);
      if ($message = self::getNoPushMessage($entity->getEntityTypeId(), $shared_id)) {
        return $message;
      }

      return self::displayNoPushReason(
            $issue
        );
    }

    return $issue;
  }

  /**
   * Get a user message on why the push failed.
   *
   * @param Exception|string $reason
   *   The reason from self::getNoPushReason()
   *
   * @return \Drupal\Core\StringTranslation\TranslatableMarkup|string
   */
  public static function displayNoPushReason($reason) {
    if ($reason instanceof \Exception) {
      return $reason->getMessage();
    }

    switch ($reason) {
      case self::NO_PUSH_REASON__HANDLER_IGNORES:
        return t('The configuration forbids the push.');

      case self::NO_PUSH_REASON__JUST_PULLED:
        return t('The entity has just been pulled and cannot be pushed immediately with the same request.');

      case self::NO_PUSH_REASON__NEVER_PUSHED:
        return t('The entity has not been pushed before, so pushing the deletion doesn\'t have any effect.');

      case self::NO_PUSH_REASON__UNCHANGED:
        return t('The entity has not changed since it\'s last push.');

      case self::NO_PUSH_REASON__NOT_REGISTERED:
        return t('The site wasn\'t registered yet.');

      default:
        return t('The entity doesn\'t have any Pool assigned.');
    }
  }

  /**
   * Helper function to push an entity and display the user the results. If
   * you want to make changes programmatically, use ::pushEntity() instead.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity to push.
   * @param string $reason
   *   {@see Flow::PUSH_*}.
   * @param string $action
   *   {@see ::ACTION_*}.
   * @param \Drupal\cms_content_sync\Entity\Flow $flow
   *   The flow to be used. If none is given, all flows that may push this.
   * @param \Drupal\cms_content_sync\Entity\Pool[] $pools
   *   The pool to be used. If not set, all relevant all pools for the flow will be used.
   * @param string[] $languages
   *   The languages to push; defaults to all.
   *
   * @return bool whether the entity is configured to be pushed or not
   */
  public static function pushEntityFromUi(EntityInterface $entity, $reason, $action, ?Flow $flow = NULL, ?array $pools = NULL, ?array $languages = NULL) {
    $messenger = \Drupal::messenger();

    try {
      $status = self::pushEntity($entity, $reason, $action, $flow, $pools, FALSE, NULL, $languages);

      if ($status) {
        $link = 'node' === $entity->getEntityTypeId() && SyncIntent::ACTION_DELETE != $action
          ? Link::createFromRoute('View progress', 'cms_content_sync.content_sync_status', ['node' => $entity->id()])->toString()
          : '';
        if (SyncIntent::ACTION_DELETE == $action) {
          $message = t('%label has been pushed to your @repository.', ['@repository' => _cms_content_sync_get_repository_name(), '%label' => $entity->getEntityTypeId()]);
        }
        else {
          $message = t('%label has been pushed to your @repository. @view_progress', [
            '@repository' => _cms_content_sync_get_repository_name(),
            '%label' => $entity->label(),
            '@view_progress' => $link,
          ]);
        }

        $update_progress = NULL;
        $is_cli = defined('STDIN') || 'cli' === php_sapi_name();
        if (!$is_cli) {
          static $embed = NULL;
          if (!$embed) {
            $embed = Embed::create(\Drupal::getContainer());
          }
          $update_progress = $embed->updateStatusBox($entity, TRUE);
        }
        if (empty($update_progress)) {
          $messenger->addMessage($message);
        }
        else {
          $messenger->addMessage([
            'message' => ['#markup' => $message],
            'update_progress' => $update_progress,
          ]);
        }

        return TRUE;
      }

      return FALSE;
    }
    catch (SyncException $e) {
      $root_exception = $e->getRootException();
      $message = $root_exception ? $root_exception->getMessage() : (
            $e->errorCode == $e->getMessage() ? '' : $e->getMessage()
            );
      if ($message) {
        $messenger->addWarning(t('Failed to push %label to your @repository (%code). Message: %message', [
          '@repository' => _cms_content_sync_get_repository_name(),
          '%label' => $entity->label(),
          '%code' => $e->errorCode,
          '%message' => $message,
        ]));

        LoggerProxy::get()->error('Failed to push %label to your @repository (%code). Message: %message<br>Error stack: %error_stack', [
          '@repository' => _cms_content_sync_get_repository_name(),
          '%label' => $entity->label(),
          '%code' => $e->errorCode,
          '%message' => $message,
          '%error_stack' => $root_exception ? $root_exception->getTraceAsString() : '',
        ]);
      }
      else {
        $messenger->addWarning(t('Failed to push %label to your @repository (%code).', [
          '@repository' => _cms_content_sync_get_repository_name(),
          '%label' => $entity->label(),
          '%code' => $e->errorCode,
        ]));

        LoggerProxy::get()->error('Failed to push %label to your @repository (%code).', [
          '@repository' => _cms_content_sync_get_repository_name(),
          '%label' => $entity->label(),
          '%code' => $e->errorCode,
        ]);
      }
      self::$noPushReasons[$entity->getEntityTypeId()][$entity->uuid()] = $e;

      return TRUE;
    }
  }

  /**
   * Helper function to push multiple entities and display the user the results.
   * If you want to make changes programmatically, use ::pushEntity() instead
   * per entity.
   *
   * @param \Drupal\Core\Entity\EntityInterface[][] $entity
   *   Every translation that should be pushed, grouped by entity.
   *   The entity to push.
   * @param string $reason
   *   {@see Flow::PUSH_*}.
   * @param string $action
   *   {@see ::ACTION_*}.
   * @param \Drupal\cms_content_sync\Entity\Flow $flow
   *   The flow to be used.
   * @param bool $run_in_order
   *   Whether the updates should be applied one after another. This also
   *   means that if an update fails, all updates after it will also fail.
   * @param int $priority
   *   The publishing priority, if not the site's default.
   * @param array $delete_translation_groups
   *   Translations to be deleted remotely, if any.
   *
   * @return bool whether the entity is configured to be pushed or not.
   */
  public static function pushEntitiesFromUi(array $translation_groups, $reason, $action, Flow $flow, bool $run_in_order = FALSE, ?int $priority = NULL, ?array $delete_translation_groups = NULL) {
    $messenger = \Drupal::messenger();

    $operation = NULL;

    $intents = [];
    $success_messages = [];

    $is_cli = defined('STDIN') || 'cli' === php_sapi_name();
    if (!$is_cli) {
      static $embed = NULL;
      if (!$embed) {
        $embed = Embed::create(\Drupal::getContainer());
      }
    }

    $translation_group_summary = [];
    foreach ($translation_groups as $update_translations) {
      $root = $update_translations[0];
      if ($root instanceof TranslatableInterface) {
        $languages = [];
        $root = $root->getUntranslated();
        foreach ($update_translations as $translation) {
          $languages[] = $translation->language()->getId();
        }
      }
      else {
        $languages = NULL;
      }
      $translation_group_summary[] = [
        'root' => $root,
        'update' => $languages,
        'delete' => NULL,
      ];
    }

    if (!empty($delete_translation_groups)) {
      foreach ($delete_translation_groups as $delete_translations) {
        $root = $delete_translations[0];
        $delete = array_slice($delete_translations, 1);
        $found = FALSE;
        foreach ($translation_group_summary as &$existing) {
          if ($existing['root']->getEntityTypeId() === $root->getEntityTypeId() && $existing['root']->id() === $root->id()) {
            $existing['delete'] = $delete;
            $found = TRUE;
            break;
          }
        }

        if (!$found) {
          $translation_group_summary[] = [
            'root' => $root instanceof TranslatableInterface ? $root->getUntranslated() : $root,
            'update' => [],
            'delete' => $delete,
          ];
        }
      }
    }

    foreach ($translation_group_summary as $actions) {
      $entity = $actions['root'];
      $update = $actions['update'];
      $delete = $actions['delete'];

      try {
        $pools = self::getPoolsToPushTo($entity, $reason, $action, $flow);

        if (!count($pools)) {
          continue;
        }

        if (!$operation) {
          $any_pool = reset($pools);
          // Ignore any push attempts if the site wasn't registered yet. This can
          // happen if the site imported Flow and Pool configuration but didn't
          // register.
          try {
            $client = $any_pool->getClient();

            if (!$client->isSiteRegistered()) {
              self::$noPushReasons[$entity->getEntityTypeId()][$entity->uuid()] = self::NO_PUSH_REASON__NOT_REGISTERED;
              continue;
            }
          }
          catch (\Exception $e) {
            self::$noPushReasons[$entity->getEntityTypeId()][$entity->uuid()] = self::NO_PUSH_REASON__NOT_REGISTERED;
            continue;
          }

          $operation =
            reset($pools)
              ->getClient()
              ->getSyndicationService()
              ->pushMultiple($flow->id);

          if ($run_in_order) {
            $operation->runInOrder(TRUE);
          }

          if ($priority !== NULL) {
            $operation->setPriority($priority);
          }
        }

        $intent = new PushIntent($flow, $pools, $reason, $action, $entity, FALSE, $update, $delete);
        $intent->setAsyncOperation($operation);
        $status = $intent->execute(FALSE);

        $intents[] = $intent;

        if ($status) {
          $link = 'node' === $entity->getEntityTypeId() && SyncIntent::ACTION_DELETE != $action
            ? Link::createFromRoute('View progress', 'cms_content_sync.content_sync_status', ['node' => $entity->id()])->toString()
            : '';
          if (SyncIntent::ACTION_DELETE == $action) {
            $message = t('%label has been pushed to your @repository.', ['@repository' => _cms_content_sync_get_repository_name(), '%label' => $entity->getEntityTypeId()]);
          }
          else {
            if ($update === NULL) {
              if (!empty($delete)) {
                $message = t('%label has been pushed to your @repository and the translation %deleted has been deleted. @view_progress', [
                  '@repository' => _cms_content_sync_get_repository_name(),
                  '%label' => $entity->label(),
                  '%deleted' => implode(', ', $delete),
                  '@view_progress' => $link,
                ]);
              }
              else {
                $message = t('%label has been pushed to your @repository. @view_progress', [
                  '@repository' => _cms_content_sync_get_repository_name(),
                  '%label' => $entity->label(),
                  '@view_progress' => $link,
                ]);
              }
            }
            elseif (count($update) === 0) {
              $message = t('The deletion of the %deleted translation of %label has been pushed to your @repository. @view_progress', [
                '@repository' => _cms_content_sync_get_repository_name(),
                '%label' => $entity->label(),
                '%deleted' => implode(', ', $delete),
                '@view_progress' => $link,
              ]);
            }
            else {
              if (!empty($delete)) {
                $message = t('%label has been pushed to your @repository with @updated updated and @deleted deleted translations. @view_progress', [
                  '@repository' => _cms_content_sync_get_repository_name(),
                  '%label' => $entity->label(),
                  '@updated' => count($update),
                  '@deleted' => count($delete),
                  '@view_progress' => $link,
                ]);
              }
              else {
                if (count($update) === 1) {
                  $message = t('%label has been pushed to your @repository. @view_progress', [
                    '@repository' => _cms_content_sync_get_repository_name(),
                    '%label' => $entity instanceof TranslatableInterface ? $entity->getTranslation($update[0])->label() : $entity->label(),
                    '@count' => count($update),
                    '@view_progress' => $link,
                  ]);
                }
                elseif (count($update) > 1) {
                  $message = t('%label has been pushed to your @repository with @count translations total. @view_progress', [
                    '@repository' => _cms_content_sync_get_repository_name(),
                    '%label' => $entity->getTranslation($update[0])->label(),
                    '@count' => count($update),
                    '@view_progress' => $link,
                  ]);
                }
              }
            }
          }

          if ($is_cli) {
            $update_progress = NULL;
          }
          else {
            $update_progress = $embed->updateStatusBox($entity, TRUE);
          }

          if (empty($update_progress)) {
            $success_messages[] = $message;
          }
          else {
            $success_messages[] = [
              'message' => ['#markup' => $message],
              'update_progress' => $update_progress,
            ];
          }
        }
      }
      catch (SyncException $e) {
        $root_exception = $e->getRootException();
        $message = $root_exception ? $root_exception->getMessage() : (
          $e->errorCode == $e->getMessage() ? '' : $e->getMessage()
              );
        if ($message) {
          $messenger->addWarning(t('Failed to push %label to your @repository (%code). Message: %message', [
            '@repository' => _cms_content_sync_get_repository_name(),
            '%label' => $entity->label(),
            '%code' => $e->errorCode,
            '%message' => $message,
          ]));

          LoggerProxy::get()->error('Failed to push %label to your @repository (%code). Message: %message<br>Error stack: %error_stack', [
            '@repository' => _cms_content_sync_get_repository_name(),
            '%label' => $entity->label(),
            '%code' => $e->errorCode,
            '%message' => $message,
            '%error_stack' => $root_exception ? $root_exception->getTraceAsString() : '',
          ]);
        }
        else {
          $messenger->addWarning(t('Failed to push %label to your @repository (%code).', [
            '@repository' => _cms_content_sync_get_repository_name(),
            '%label' => $entity->label(),
            '%code' => $e->errorCode,
          ]));

          LoggerProxy::get()->error('Failed to push %label to your @repository (%code).', [
            '@repository' => _cms_content_sync_get_repository_name(),
            '%label' => $entity->label(),
            '%code' => $e->errorCode,
          ]);
        }
        self::$noPushReasons[$entity->getEntityTypeId()][$entity->uuid()] = $e;
      }
    }

    if ($operation) {
      try {
        $operation->execute();
      }
      catch (SyncCoreException $e) {
        foreach ($intents as $intent) {
          $intent->handleFinishedAsyncOperation($e);
        }
        return FALSE;
      }

      foreach ($intents as $intent) {
        $intent->handleFinishedAsyncOperation(NULL);
      }

      foreach ($success_messages as $message) {
        $messenger->addMessage($message);
      }
    }

    return TRUE;
  }

  /**
   * Push the provided entity along with the processed entity by embedding it
   * right into the current entity. This means the embedded entity can't be used
   * outside of it's parent entity in any way. This is used for field
   * collections right now.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The referenced entity to push as well.
   * @param array $details
   *   {@see SyncIntent::getEmbedEntityDefinition}.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   * @throws \Drupal\Core\Entity\EntityStorageException
   * @throws \Drupal\cms_content_sync\Exception\SyncException
   * @throws \GuzzleHttp\Exception\GuzzleException
   *
   * @return array|object the definition you can store via {@see SyncIntent::setField} and on the other end receive via {@see SyncIntent::getField}
   */
  public function embed($entity, $details = NULL) {
    return $this->embedForFlowAndPools($entity, $details, $this->flow, $this->pools);
  }

  /**
   * Push the provided entity as a dependency meaning the referenced entity
   * is available before this entity so it can be referenced on the remote site
   * immediately like bricks or paragraphs.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The referenced entity to push as well.
   * @param array $details
   *   {@see SyncIntent::getEmbedEntityDefinition}.
   * @param bool $push_to_same_pool
   * @param string|null $view_url
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   * @throws \Drupal\Core\Entity\EntityStorageException
   * @throws \Drupal\cms_content_sync\Exception\SyncException
   * @throws \GuzzleHttp\Exception\GuzzleException
   *
   * @return array|object the definition you can store via {@see SyncIntent::setField} and on the other end receive via {@see SyncIntent::getField}
   */
  public function addDependency($entity, $details = NULL, $push_to_same_pool = TRUE, ?string $view_url = NULL) {
    $embed = reset($this->pools)->getClient() instanceof SyncCore || in_array($entity->getEntityTypeId(), ContentSyncSettings::getInstance()->getEmbedEntities());
    if ($embed) {
      $result = $this->pushReference($entity, $details, TRUE, $push_to_same_pool, TRUE);
      if ($result) {
        return $result;
      }
      $all_pools = NULL;
    }
    else {
      $all_pools = $this->pushReference($entity, $details, TRUE, $push_to_same_pool);
    }

    // Not pushed? Just using our current pool then to de-reference it at the remote site if the entity exists.
    if (empty($all_pools)) {
      return $this->operation->addReference(
            $entity->getEntityTypeId(),
            $entity->bundle(),
            $entity->uuid(),
            EntityHandlerPluginManager::getIdOrNull($entity),
            Flow::getEntityTypeVersion($entity->getEntityTypeId(), $entity->bundle()),
            $this->getPoolIds(),
            $entity->language()->getId(),
            $entity->label(),
            $details,
        $view_url
        );
    }

    $result = NULL;

    foreach ($all_pools as $pool) {
      $result = $this->operation->addDependency(
            $entity->getEntityTypeId(),
            $entity->bundle(),
            $entity->uuid(),
            EntityHandlerPluginManager::getIdOrNull($entity),
            Flow::getEntityTypeVersion($entity->getEntityTypeId(), $entity->bundle()),
            [$pool->id()],
            $entity->language()->getId(),
            $entity->label(),
            $details
        );
    }

    return $result;
  }

  /**
   * Push the provided entity as a simple reference. There is no guarantee the
   * referenced entity will be available on the remote site as well, but if it
   * is, it will be de-referenced. If you need the referenced entity to be available,
   * use {@see PushIntent::addDependency} instead.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The referenced entity to push as well.
   * @param array $details
   *   {@see SyncIntent::getEmbedEntityDefinition}.
   * @param string|null $view_url
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   * @throws \Drupal\Core\Entity\EntityStorageException
   * @throws \Drupal\cms_content_sync\Exception\SyncException
   * @throws \GuzzleHttp\Exception\GuzzleException
   *
   * @return array|object the definition you can store via {@see SyncIntent::setField} and on the other end receive via {@see SyncIntent::getField}
   */
  public function addReference($entity, $details = NULL, ?string $view_url = NULL) {
    // Check if the Pool has been selected manually. In this case, we need to embed the entity despite the AUTO PUSH not being set.
    $statuses = EntityStatus::getInfosForEntity($entity->getEntityTypeId(), $entity->uuid(), ['flow' => $this->flow->id()]);

    if (in_array($entity->getEntityTypeId(), ContentSyncSettings::getInstance()->getEmbedEntities())) {
      $result = NULL;
      $pools = [];
      foreach ($statuses as $status) {
        if ($status->isManualPushEnabled()) {
          $pools[] = $status->getPool();
        }
      }
      if (count($pools)) {
        $result = $this->embedForFlowAndPools($entity, $details, $status->getFlow(), $pools);
      }

      if ($result) {
        return $result;
      }

      // Not pushed? Just using our current pool then to de-reference it at the remote site if the entity exists.
      return $this->operation->addReference(
            $entity->getEntityTypeId(),
            $entity->bundle(),
            $entity->uuid(),
            EntityHandlerPluginManager::getIdOrNull($entity),
            Flow::getEntityTypeVersion($entity->getEntityTypeId(), $entity->bundle()),
            $this->getPoolIds(),
            $entity->language()->getId(),
            $entity->label(),
            $details,
            $view_url
        );
    }

    foreach ($statuses as $status) {
      if ($status->isManualPushEnabled()) {
        // This is only relevant for dependencies.
        // If the mode is "all" or "manual" we must not add them as a dependency as otherwise this can result in pushing referenced entities endlessly.
        if ($this->flow->getController()->canPushEntity($entity, PushIntent::PUSH_AS_DEPENDENCY)) {
          return $this->addDependency($entity, $details, FALSE, $view_url);
        }
      }
    }

    $all_pools = $this->pushReference($entity, $details, FALSE);

    // Not pushed? Just using our current pool then to de-reference it at the remote site if the entity exists.
    if (empty($all_pools)) {
      return $this->operation->addReference(
            $entity->getEntityTypeId(),
            $entity->bundle(),
            $entity->uuid(),
            EntityHandlerPluginManager::getIdOrNull($entity),
            Flow::getEntityTypeVersion($entity->getEntityTypeId(), $entity->bundle()),
            $this->getPoolIds(),
            $entity->language()->getId(),
            $entity->label(),
            $details,
            $view_url
        );
    }

    $result = NULL;

    foreach ($all_pools as $pool) {
      $result = $this->operation->addReference(
            $entity->getEntityTypeId(),
            $entity->bundle(),
            $entity->uuid(),
            EntityHandlerPluginManager::getIdOrNull($entity),
            Flow::getEntityTypeVersion($entity->getEntityTypeId(), $entity->bundle()),
            [$pool->id()],
            $entity->language()->getId(),
            $entity->label(),
            $details,
            $view_url
        );
    }

    return $result;
  }

  /**
   * Set the value of the given field. By default every field handler
   * will have a field available for storage when pulling / pushing that
   * accepts all non-associative array-values. Within this array you can
   * use the following types: array, associative array, string, integer, float,
   * boolean, NULL. These values will be JSON encoded when pushing and JSON
   * decoded when pulling. They will be saved in a structured database by
   * Sync Core in between, so you can't pass any non-array value by default.
   *
   * @param string $name
   *   The name of the field in question.
   * @param mixed $value
   *   The value to store.
   * @param bool $requried
   *   Whether empty values can be skipped.
   */
  public function setProperty($name, $value, $required = FALSE) {
    // Don't need to store empty values.
    if (NULL === $value) {
      return;
    }

    if (!$required) {
      if ('' === $value || (is_array($value) && (0 === count($value) || (1 === count($value) && isset($value[0]) && is_array($value[0]) && 0 === count($value[0]))))) {
        return;
      }
    }

    $this->operation->setProperty($name, $value, $this->activeLanguage);
  }

  /**
   * Get a property value as it was set before.
   *
   * @return mixed
   */
  public function getProperty(string $name) {
    return $this->operation->getProperty($name);
  }

  /**
   *
   */
  protected function getHandler() {
    $config = $this->flow->getController()->getEntityTypeConfig($this->entityType, $this->bundle);

    return $this->flow->getController()->getEntityTypeHandler($this->entityType, $this->bundle, $config);
  }

  /**
   * Save that the pull for the given entity failed.
   *
   * @param string $failure_reason
   *   See PushIntent::PUSH_FAILURE_*.
   * @param null|string $message
   *   An optional message accompanying this error.
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   */
  protected function saveFailedPush($failure_reason, $message = NULL) {
    $soft_fails = [
      PushIntent::PUSH_FAILED_HANDLER_DENIED,
      PushIntent::PUSH_FAILED_UNCHANGED,
    ];

    $soft = in_array($failure_reason, $soft_fails);

    $this->entity_status->didPushFail(TRUE, $soft, [
      'error' => $failure_reason,
      'action' => $this->getAction(),
      'reason' => $this->getReason(),
      'message' => $message,
    ]);

    $this->entity_status->save();
  }

  /**
   * @return int
   */
  protected function getRequestTime() {
    return (int) $_SERVER['REQUEST_TIME'];
  }

  /**
   * Get the changed date of the entity. Not all entities provide the required attribute and even those aren't
   * consistently saving it so this method takes care of these exceptions.
   *
   * @todo Check if we should remove this as we're no longer using this changed
   *   date for deciding whether to push an entity or not (using hashes now).
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   * @param bool $specific_translation
   *
   * @return int
   */
  protected function getEntityChangedTime($entity, $specific_translation = FALSE) {
    $request_time = $this->getRequestTime();
    $push = $request_time;

    if ($entity instanceof EntityChangedInterface) {
      $push = $specific_translation ? (int) $entity->getChangedTime() : (int) $entity->getChangedTimeAcrossTranslations();

      // Check if any bricks were updated during this request that this specific entity is referencing
      // Quick edit doesn't update the changed date of the node...... so we have to go and see manually if anything
      // changed by caching it....
      if ($push < $request_time && $this->isQuickEdited) {
        return $request_time;
      }
    }

    if (EntityHandlerPluginManager::isEntityTypeFieldable($entity->getEntityTypeId())) {
      /**
       * @var \Drupal\Core\Entity\EntityFieldManager $entity_field_manager
       */
      $entity_field_manager = \Drupal::service('entity_field.manager');

      /**
       * @var \Drupal\Core\Field\FieldDefinitionInterface[] $fields
       */
      $fields = $entity_field_manager->getFieldDefinitions($entity->getEntityTypeId(), $entity->bundle());

      // Elements that are inline edited in other forms like media elements edited within node forms don't get their
      // timestamp updated even though the file attributes may change like the focal point. So we are checking for file
      // reference fields and check if they were updated in the meantime.
      foreach ($fields as $key => $field) {
        if ('image' === $field->getType()) {
          $data = $entity->get($key)->getValue();

          foreach ($data as $delta => $value) {
            if (empty($value['target_id'])) {
              continue;
            }

            $entityTypeManager = \Drupal::entityTypeManager();
            $storage = $entityTypeManager->getStorage('file');

            $target_id = $value['target_id'];
            $reference = $storage->load($target_id);

            if (!$reference) {
              continue;
            }

            $sub = $this->getEntityChangedTime($reference, $specific_translation);
            if ($sub > $push) {
              $push = $sub;
            }
          }
        }
      }
    }

    // File entities timestamp doesn't change when focal point is updated and crop entity doesn't provide a changed date.
    if ('file' === $entity->getEntityTypeId()) {
      /**
       * @var \Drupal\file\FileInterface $entity
       */

      // Handle crop entities.
      $moduleHandler = \Drupal::service('module_handler');
      if ($moduleHandler->moduleExists('crop') && $moduleHandler->moduleExists('focal_point')) {
        if (Crop::cropExists($entity->getFileUri(), 'focal_point')) {
          $crop = Crop::findCrop($entity->getFileUri(), 'focal_point');
          if ($crop) {
            $info = EntityStatus::getInfoForEntity('file', $entity->uuid(), $this->flow->id(), reset($this->pools)->id());

            if ($info) {
              $position = $crop->position();
              $last = $info->getData('crop');
              if (empty($last) || $position['x'] !== $last['x'] || $position['y'] !== $last['y']) {
                $push = $this->getRequestTime();
              }
            }
          }
        }
      }
    }

    return $push;
  }

  /**
   * Check whether the entity changed at all since the last push.
   *
   * @return bool
   */
  protected function entityChanged() {
    $last_hash = $this->entity_status->getEntityPushHash();
    $new_hash = $this->operation->getEntityHash();

    return $last_hash !== $new_hash;
  }

  /**
   * Push the provided entity along with the processed entity by embedding it
   * right into the current entity. This means the embedded entity can't be used
   * outside of it's parent entity in any way. This is used for field
   * collections right now.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The referenced entity to push as well.
   * @param null|array $details
   *   {@see SyncIntent::getEmbedEntityDefinition}.
   * @param \Drupal\cms_content_sync\Entity\Flow $flow
   * @param \Drupal\cms_content_sync\Entity\Pool[] $pools
   * @param mixed $optional
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   * @throws \Drupal\Core\Entity\EntityStorageException
   * @throws \Drupal\cms_content_sync\Exception\SyncException
   * @throws \GuzzleHttp\Exception\GuzzleException
   *
   * @return array|object the definition you can store via {@see SyncIntent::setField} and on the other end receive via {@see SyncIntent::getField}
   */
  protected function embedForFlowAndPools($entity, $details, $flow, $pools, $optional = FALSE) {
    $start = microtime(TRUE);

    $individual_translation = $this->isIndividualTranslation();
    if ($individual_translation) {
      if ($entity instanceof TranslatableInterface) {
        if ($entity->hasTranslation($this->individualLanguage)) {
          $entity = $entity->getTranslation($this->individualLanguage);
        }
      }
    }

    /**
     * @var PushIntent $embed_entity
     */
    $embed_entity = PushIntent::pushEntity(
          $entity,
          PushIntent::PUSH_AS_DEPENDENCY,
          SyncIntent::ACTION_CREATE,
          $flow,
          $pools,
          TRUE,
          $this->individualLanguage
      );

    $this->childTime += microtime(TRUE) - $start;

    if (!$embed_entity) {
      if ($optional) {
        return NULL;
      }

      throw new SyncException(SyncException::CODE_UNEXPECTED_EXCEPTION, NULL, 'Failed to embed entity ' . $entity->getEntityTypeId() . ' ' . $entity->uuid() . ': ' . PushIntent::getNoPushReason($entity, TRUE));
    }

    $this->addCacheTags($embed_entity->getCacheTags());

    $result = $this->operation->embed(
          $entity->getEntityTypeId(),
          $entity->bundle(),
          $entity->uuid(),
          EntityHandlerPluginManager::getIdOrNull($entity),
          Flow::getEntityTypeVersion($entity->getEntityTypeId(), $entity->bundle()),
          $embed_entity->getOperation(),
          $details
      );

    $this->embeddedPushIntents[] = $embed_entity;

    return $result;
  }

  /**
   * @param \Drupal\Core\Entity\EntityInterface $entity
   * @param array $details
   * @param bool $dependency
   * @param bool $push_to_same_pool
   * @param bool $embed
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   * @throws \Drupal\cms_content_sync\Exception\SyncException
   *
   * @return \Drupal\cms_content_sync\Entity\Pool[] The Pools that were used
   */
  protected function pushReference($entity, $details, $dependency, $push_to_same_pool = FALSE, $embed = FALSE) {
    $embed_result = NULL;

    try {
      $all_pools = Pool::getAll();
      $all_pools = $this->flow->getController()->getPoolsToPushTo($entity, PushIntent::PUSH_AS_DEPENDENCY, SyncIntent::ACTION_CREATE, TRUE);
      $preferred_pools = [];
      foreach ($this->pools as $pool) {
        if (isset($all_pools[$pool->id]) || $push_to_same_pool) {
          $preferred_pools[$pool->id] = $pool;
        }
      }
      $all_pools = $preferred_pools + $all_pools;
      $used_pools = [];

      $version = Flow::getEntityTypeVersion($entity->getEntityTypeId(), $entity->bundle());

      $flow = $this->flow;
      if (!$flow->getController()->canAddEntityAsDependency($entity)) {
        return $embed ? $embed_result : $used_pools;
      }

      $export_pools = $flow->getController()->getEntityTypeConfig($entity->getEntityTypeId(), $entity->bundle())['export_pools'];

      // Make sure the first pool we try is the pool of the current parent
      // push operation.
      $preferred_pools = [];
      foreach ($this->pools as $pool) {
        if (isset($export_pools[$pool->id])) {
          $preferred_pools[$pool->id] = $export_pools[$pool->id];
        }
      }
      $export_pools = $preferred_pools + $export_pools;

      foreach ($export_pools as $pool_id => $behavior) {
        if (empty($all_pools[$pool_id]) || in_array($all_pools[$pool_id], $used_pools)) {
          continue;
        }

        if (Pool::POOL_USAGE_FORBID == $behavior) {
          continue;
        }

        // If this entity was newly created, it won't have any groups to push to
        // selected, unless they're FORCED. In this case we add default sync
        // groups based on the parent entity, as you would expect.
        if ($dependency) {
          if (!isset($all_pools[$pool_id])) {
            continue;
          }
          $pool = $all_pools[$pool_id];
          $info = EntityStatus::getInfoForEntity($entity->getEntityTypeId(), $entity->uuid(), $flow, $pool);

          if (!$info) {
            if (!$push_to_same_pool) {
              continue;
            }
            $info = EntityStatus::create([
              'flow' => $flow->id,
              'pool' => $pool->id,
              'entity_type' => $entity->getEntityTypeId(),
              'entity_uuid' => $entity->uuid(),
              'entity_type_version' => $version,
              'flags' => 0,
            ]);
          }

          $info->isPushEnabled(NULL, TRUE);
          if ($embed) {
            $info->setLastPush($this->getEntityChangedTime($entity));
          }
          $info->save();
        }
        else {
          $pool = $all_pools[$pool_id];
          if (Pool::POOL_USAGE_ALLOW == $behavior) {
            $info = EntityStatus::getInfoForEntity($entity->getEntityTypeId(), $entity->uuid(), $flow, $pool);
            if (!$info || !$info->isPushEnabled()) {
              continue;
            }
          }
        }

        $info = EntityStatus::getInfoForEntity($entity->getEntityTypeId(), $entity->uuid(), $flow, $pool);

        // In case the handler denied pushing the entity, we simply ignore the attempt.
        if (!$info || !$info->getLastPush($this->individualLanguage)) {
          // Unless we are referencing our parent entity that is also being pushed right now
          // e.g. a menu item will reference it's parent node but the parent will trigger
          // the menu item push so the status entity isn't there yet.
          if (self::isPushing($entity->getEntityTypeId(), $entity->uuid())) {
            continue;
          }
        }

        $used_pools[] = $all_pools[$pool_id];
      }

      if ($dependency) {
        if ($embed) {
          $new_embed_result = $this->embedForFlowAndPools($entity, $details, $flow, $used_pools, FALSE);
          if ($new_embed_result && !$embed_result) {
            $embed_result = $new_embed_result;
          }
        }
        else {
          $individual_translation = $this->isIndividualTranslation();
          if ($individual_translation) {
            if ($entity instanceof TranslatableInterface) {
              if ($entity->hasTranslation($this->individualLanguage)) {
                $entity = $entity->getTranslation($this->individualLanguage);
              }
            }
          }
          PushIntent::pushEntity($entity, PushIntent::PUSH_AS_DEPENDENCY, SyncIntent::ACTION_CREATE, $flow, $used_pools, FALSE, $individual_translation);
        }
      }
    }
    catch (\Exception $e) {
      $this->saveFailedPush(PushIntent::PUSH_FAILED_DEPENDENCY_PUSH_FAILED, $e->getMessage());

      throw new SyncException(SyncException::CODE_UNEXPECTED_EXCEPTION, $e);
    }

    return $embed ? $embed_result : $used_pools;
  }

  /**
   * Whether or not the user is allowed to select what languages to push.
   * Depends on the feature flags set for this space at the Sync Core.
   *
   * @return bool
   */
  public static function canHandleTranslationsIndependently() {
    static $result = NULL;
    if ($result !== NULL) {
      return $result;
    }
    $client = SyncCoreFactory::getSyncCoreV2();
    return $result = $client->featureEnabled(ISyncCore::FEATURE_REQUEST_PER_TRANSLATION) && $client->featureEnabled(ISyncCore::FEATURE_SKIP_UNCHANGED_TRANSLATIONS);
  }

}

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

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