cms_content_sync-3.0.x-dev/src/Plugin/EntityHandlerBase.php

src/Plugin/EntityHandlerBase.php
<?php

namespace Drupal\cms_content_sync\Plugin;

use Drupal\cms_content_sync\Controller\ContentSyncSettings;
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\EntityStatusProxy;
use Drupal\cms_content_sync\Event\BeforeEntityPull;
use Drupal\cms_content_sync\Event\BeforeEntityPush;
use Drupal\cms_content_sync\Exception\SyncException;
use Drupal\cms_content_sync\Plugin\Type\EntityHandlerPluginManager;
use Drupal\cms_content_sync\PullIntent;
use Drupal\cms_content_sync\PushIntent;
use Drupal\cms_content_sync\SyncIntent;
use Drupal\Core\Config\Entity\ConfigEntityStorage;
use Drupal\Core\Entity\EntityChangedInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\FieldableEntityInterface;
use Drupal\Core\Entity\RevisionableEntityBundleInterface;
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
use Drupal\Core\Entity\SynchronizableInterface;
use Drupal\Core\Entity\TranslatableInterface;
use Drupal\Core\Form\FormStateInterface;
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
use Drupal\Core\Plugin\PluginBase;
use Drupal\Core\Render\RenderContext;
use Drupal\crop\Entity\Crop;
use Drupal\menu_link_content\Plugin\Menu\MenuLinkContent;
use Drupal\node\NodeInterface;
use Drupal\paragraphs\ParagraphInterface;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerInterface;
use EdgeBox\SyncCore\Interfaces\Configuration\IDefineEntityType;
use EdgeBox\SyncCore\V2\Configuration\DefineProperty;
use EdgeBox\SyncCore\V2\Raw\Model\RemoteEntityListResponse;
use EdgeBox\SyncCore\V2\Raw\Model\RemoteEntitySummary;
use EdgeBox\SyncCore\V2\Raw\Model\RemoteEntityTranslationDetails;
use EdgeBox\SyncCore\V2\Raw\Model\RemoteRequestQueryParamsEntityList;

/**
 * Common base class for entity handler plugins.
 *
 * @see \Drupal\cms_content_sync\Annotation\EntityHandler
 * @see \Drupal\cms_content_sync\Plugin\EntityHandlerInterface
 * @see plugin_api
 *
 * @ingroup third_party
 */
abstract class EntityHandlerBase extends PluginBase implements ContainerFactoryPluginInterface, EntityHandlerInterface {
  public const USER_PROPERTY = NULL;
  public const USER_REVISION_PROPERTY = NULL;
  public const REVISION_TRANSLATION_AFFECTED_PROPERTY = NULL;

  /**
   * A logger instance.
   *
   * @var \Psr\Log\LoggerInterface
   */
  protected $logger;

  /**
   * The entity type name.
   *
   * @var string
   */
  protected $entityTypeName;

  /**
   * The entiy type bundle name.
   *
   * @var string
   */
  protected $bundleName;

  /**
   * The entity handler settings.
   *
   * @var array
   */
  protected $settings;

  /**
   * A sync instance.
   *
   * @var \Drupal\cms_content_sync\Entity\Flow
   */
  protected $flow;

  /**
   * Constructs a Drupal\rest\Plugin\ResourceBase object.
   *
   * @param array $configuration
   *   A configuration array containing information about the plugin instance.
   * @param string $plugin_id
   *   The plugin_id for the plugin instance.
   * @param mixed $plugin_definition
   *   The plugin implementation definition.
   * @param \Psr\Log\LoggerInterface $logger
   *   A logger instance.
   */
  public function __construct(array $configuration, $plugin_id, $plugin_definition, LoggerInterface $logger) {
    parent::__construct($configuration, $plugin_id, $plugin_definition);
    $this->logger = $logger;
    $this->entityTypeName = $configuration['entity_type_name'];
    $this->bundleName = $configuration['bundle_name'];
    $this->settings = $configuration['settings'];
    $this->flow = $configuration['sync'];
  }

  /**
   * {@inheritdoc}
   */
  public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
    return new static(
    $configuration,
    $plugin_id,
    $plugin_definition,
    LoggerProxy::get()
    );
  }

  /**
   * {@inheritdoc}
   */
  public function getAllowedPushOptions() {
    return [
      PushIntent::PUSH_DISABLED,
      PushIntent::PUSH_AUTOMATICALLY,
      PushIntent::PUSH_AS_DEPENDENCY,
      PushIntent::PUSH_MANUALLY,
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function getAllowedPullOptions() {
    return [
      PullIntent::PULL_DISABLED,
      PullIntent::PULL_MANUALLY,
      PullIntent::PULL_AUTOMATICALLY,
      PullIntent::PULL_AS_DEPENDENCY,
    ];
  }

  /**
   * {@inheritdoc}
   */
  public function updateEntityTypeDefinition(IDefineEntityType|DefineProperty &$definition) {
    /**
     * @var \Drupal\Core\Entity\EntityTypeInterface $entity_type_entity
     */
    $entity_type_entity = \Drupal::service('entity_type.manager')
      ->getStorage($this->entityTypeName)
      ->getEntityType();

    $key = $entity_type_entity->getKey('label');

    if ($this->hasLabelProperty() && $key) {
      $definition->addStringProperty($key, $key, FALSE, FALSE, 'string')->setLocalized(TRUE)->setShared(TRUE);
    }

    // We add the bundle as an optional key because older versions of Content
    // Sync didn't export it at all. It's also not required for the sync as
    // we're using another meta information to store the bundle information
    // in a standardized way.
    $key = $entity_type_entity->getKey('bundle');
    if ($key) {
      $definition->addStringProperty($key, $key, FALSE, FALSE, 'bundle')->setLocalized(FALSE)->setShared(TRUE);
    }

    if ($bundle_entity_type = $entity_type_entity->getBundleEntityType()) {
      $bundle_type = \Drupal::service('entity_type.manager')->getStorage($bundle_entity_type)->load($this->bundleName);
    }
    else {
      $bundle_type = $entity_type_entity;
    }

    if ($bundle_type && method_exists($bundle_type, 'getDescription')) {
      $description = $bundle_type->getDescription();
      if ($description) {
        if (!is_string($description)) {
          $description = $description . '';
        }
        $definition->setDescription($description);
      }
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getHandlerSettings($current_values, $type = 'both') {
    $options = [];

    $no_menu_link_push = [
      'brick',
      'menu_link_content',
      'paragraph',
      'paragraphs_library_item',
    ];

    if (!in_array($this->entityTypeName, $no_menu_link_push) && 'pull' !== $type) {
      $options['export_menu_items'] = [
        '#type' => 'checkbox',
        '#title' => 'Push menu items',
        '#default_value' => isset($current_values['export_menu_items']) && 0 === $current_values['export_menu_items'] ? 0 : 1,
      ];
    }

    return $options;
  }

  /**
   * {@inheritdoc}
   */
  public function validateHandlerSettings(array &$form, FormStateInterface $form_state, string $entity_type_name, string $bundle_name, $current_values) {
    // No settings means no validation.
  }

  /**
   * Execute the pull after checking ignore-cases and firing events.
   *
   * {@inheritdoc}
   */
  protected function doPull(PullIntent $intent) {
    $action = $intent->getAction();

    /**
     * @var null|\Drupal\Core\Entity\EntityInterface $entity
     */
    $entity = $intent->getEntity();

    if (SyncIntent::ACTION_DELETE == $action) {
      if ($entity) {
        return $this->deleteEntity($entity);
      }
      // Already done means success.
      if ($intent->getEntityStatus()->isDeleted()) {
        return TRUE;
      }

      $intent->setIgnoreMessage('The entity does not exist.');

      return FALSE;
    }
    if ($entity) {
      if ($bundle_entity_type = $entity->getEntityType()->getBundleEntityType()) {
        $bundle_entity_type = \Drupal::entityTypeManager()->getStorage($bundle_entity_type)->load($entity->bundle());
        if (($bundle_entity_type instanceof RevisionableEntityBundleInterface && $bundle_entity_type->shouldCreateNewRevision()) || 'paragraph' == $entity->getEntityTypeId()) {
          $entity->setNewRevision(TRUE);
          $entity->setRevisionTranslationAffected(TRUE);
        }
      }
    }
    else {
      $intent->startTimer('create-new');
      $entity = $this->createNew($intent);
      $intent->stopTimer('create-new');

      if (!$entity) {
        throw new SyncException(SyncException::CODE_ENTITY_API_FAILURE);
      }

      $intent->setEntity($entity);
    }

    if ($entity instanceof FieldableEntityInterface && !$this->setEntityValues($intent)) {
      $intent->setIgnoreMessage('Failed to set entity values.');

      return FALSE;
    }

    return TRUE;
  }

  /**
   * Pull the remote entity.
   *
   * {@inheritdoc}
   */
  public function pull(PullIntent $intent) {
    $action = $intent->getAction();

    /**
     * @var null|\Drupal\Core\Entity\EntityInterface $entity
     */
    $entity = $intent->getEntity();

    $intent->startTimer('before-pull-event');
    // Allow other modules to extend the EntityHandlerBase pull.
    // Dispatch ExtendEntityPull.
    $event = new BeforeEntityPull($entity, $intent);
    \Drupal::service('event_dispatcher')->dispatch($event, BeforeEntityPull::EVENT_NAME);
    $intent->stopTimer('before-pull-event');

    // Allow other modules to ask Content Sync to ignore the pull operation.
    if ($event->ignore) {
      $intent->setIgnoreMessage('An event handler chose to ignore this entity explicitly.');

      return FALSE;
    }

    if ($this->ignorePull($intent)) {
      // Still pull updates on translations if only the root language should
      // be ignored. E.g. because the skip-unchanged flag is set and the
      // root translation didn't change but another translation did.
      if ($entity && $this->isEntityTypeTranslatable($entity) && SyncIntent::ACTION_DELETE !== $action) {
        if ($bundle_entity_type = $entity->getEntityType()->getBundleEntityType()) {
          $bundle_entity_type = \Drupal::entityTypeManager()->getStorage($bundle_entity_type)->load($entity->bundle());
          if (($bundle_entity_type instanceof RevisionableEntityBundleInterface && $bundle_entity_type->shouldCreateNewRevision()) || 'paragraph' == $entity->getEntityTypeId()) {
            $entity->setNewRevision(TRUE);
            $entity->setRevisionTranslationAffected(TRUE);
          }
        }

        // Return TRUE if a translation is updated OR deleted.
        return $this->pullTranslations($intent, $entity);
      }

      return FALSE;
    }

    $result = $this->doPull($intent);
    if ($result) {
      $entity = $intent->getEntity();

      if ($entity) {
        $new_version = $intent->getVersionId($this->isEntityTypeTranslatable($entity) && $entity->isDefaultTranslation());
        if ($new_version) {
          $intent->getEntityStatus()->setTranslationData($entity->language()->getId(), EntityStatus::DATA_TRANSLATIONS_LAST_PULLED_VERSION_ID, $new_version);
        }
      }
    }

    return $result;
  }

  /**
   * Get the entity view url.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity to check for.
   *
   * @throws \Drupal\cms_content_sync\Exception\SyncException
   *
   * @return string
   *   Returns the entity view url.
   */
  public function getViewUrl(EntityInterface $entity) {
    if (!$entity->hasLinkTemplate('canonical')) {
      if (!$entity->hasLinkTemplate('edit-form')) {
        throw new SyncException('No canonical link template found for entity ' . $entity->getEntityTypeId() . '.' . $entity->bundle() . ' ' . $entity->id() . '. Please overwrite the handler to provide a URL.');
      }

      // Use the edit-form as a fallback if there's no canonical link template.
      try {
        $url = $entity->toUrl('edit-form', [
          'absolute' => TRUE,
          'language' => $entity->language(),
          // Workaround for PathProcessorAlias::processOutbound to explicitly ignore us
          // as we always want the pure, unaliased e.g. /node/:id path because
          // we don't use the URL for end-users but for editors and it has to
          // be reliable (aliases can be removed or change).
          'alias' => TRUE,
        ]);

        return $url->toString();
      }
      catch (\Exception $e) {
        throw new SyncException(SyncException::CODE_UNEXPECTED_EXCEPTION, $e);
      }
    }

    try {
      $url = $entity->toUrl('canonical', [
        'absolute' => TRUE,
        'language' => $entity->language(),
            // Workaround for PathProcessorAlias::processOutbound to explicitly ignore us
            // as we always want the pure, unaliased e.g. /node/:id path because
            // we don't use the URL for end-users but for editors and it has to
            // be reliable (aliases can be removed or change).
        'alias' => TRUE,
      ]);

      return $url->toString();
    }
    catch (\Exception $e) {
      throw new SyncException(SyncException::CODE_UNEXPECTED_EXCEPTION, $e);
    }
  }

  /**
   * {@inheritdoc}
   */
  public function getForbiddenFields() {
    /**
     * @var \Drupal\Core\Entity\EntityTypeInterface $entity_type_entity
     */
    $entity_type_entity = \Drupal::service('entity_type.manager')
      ->getStorage($this->entityTypeName)
      ->getEntityType();

    return [
      // These basic fields are already taken care of, so we ignore them
      // here.
      ...EntityHandlerPluginManager::mapById($entity_type_entity) ? [] : [$entity_type_entity->getKey('id')],
      $entity_type_entity->getKey('revision'),
      $entity_type_entity->getKey('bundle'),
      $entity_type_entity->getKey('uuid'),
      $entity_type_entity->getKey('label'),
      // These are not relevant or misleading when synchronized.
      'revision_default',
      'revision_translation_affected',
      'content_translation_outdated',
    ];
  }

  /**
   * Whether the given entity is published or not.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *
   * @return bool
   */
  protected function isPublished(EntityInterface $entity) {
    $entity_type = $entity->getEntityType();
    $status_property = EntityHandlerPluginManager::getEntityTypeStatusProperty($entity_type, $entity->bundle());
    if ($status_property) {
      return (bool) $entity->get($status_property)->value;
    }
    // Entities that don't have a "status" property are published by default,
    // usually based on their parent entity.
    return TRUE;
  }

  /**
   * {@inheritdoc}
   */
  public function push(PushIntent $intent, ?EntityInterface $entity = NULL) {
    if ($this->ignorePush($intent)) {
      return FALSE;
    }

    if (!$entity) {
      $entity = $intent->getEntity();
    }

    // Base info.
    $name = $this->getEntityName($entity, $intent);
    // Focal point and cohesion layout have no label for example.
    if (!$name) {
      $name = 'Unnamed ' . $entity->getEntityTypeId() . '.' . $entity->bundle();
    }
    $intent->getOperation()->setName($name, $intent->getActiveLanguage());

    $entity_type = \Drupal::service('entity_type.manager')
      ->getStorage($this->entityTypeName)
      ->getEntityType();

    $label_key = $entity_type->getKey('label');
    if ($this->hasLabelProperty() && $name && $label_key) {
      $intent->setProperty($label_key, $name);
    }

    $bundle_key = $entity_type->getKey('bundle');
    if ($bundle_key && $entity->bundle()) {
      $intent->setProperty($bundle_key, $entity->bundle());
    }

    $intent->getOperation()->setPublished($this->isPublished($entity), $intent->getActiveLanguage());

    // Menu items.
    if ($this->pushReferencedMenuItems()) {
      $intent->startTimer('menu-items');
      $menu_link_manager = \Drupal::service('plugin.manager.menu.link');
      /**
       * @var \Drupal\Core\Menu\MenuLinkManager $menu_link_manager
       */
      $menu_items = $menu_link_manager->loadLinksByRoute('entity.' . $this->entityTypeName . '.canonical', [$this->entityTypeName => $entity->id()]);
      $values = [];

      $form_values = _cms_content_sync_submit_cache($entity->getEntityTypeId(), $entity->uuid());

      foreach ($menu_items as $menu_item) {
        if (!($menu_item instanceof MenuLinkContent)) {
          continue;
        }

        /**
         * @var \Drupal\menu_link_content\Entity\MenuLinkContent $item
         */
        $item = \Drupal::service('entity.repository')
          ->loadEntityByUuid('menu_link_content', $menu_item->getDerivativeId());
        if (!$item) {
          continue;
        }

        // Menu item has just been disabled => Ignore push in this case.
        if (isset($form_values['menu']) && $form_values['menu']['id'] == 'menu_link_content:' . $item->uuid()) {
          if (!$form_values['menu']['enabled']) {
            continue;
          }
        }

        $details = [];
        $details['enabled'] = $item->get('enabled')->value;

        try {
          $values[] = $intent->addDependency($item, $details);
        }
        // Menu items are not a hard dependency, so we ignore failures (e.g. the handler denying the push as the menu is restricted).
        catch (\Exception $e) {
        }
      }

      $intent->setProperty('menu_items', $values);
      $intent->stopTimer('menu-items');
    }

    // Preview.
    $view_mode = $this->flow->getController()->getPreviewType($entity->getEntityTypeId(), $entity->bundle());
    if (Flow::PREVIEW_DISABLED != $view_mode) {
      $intent->startTimer('preview');
      // Always use the standard theme for rendering the previews.
      $config_factory = \Drupal::service('config.factory');
      $theme_manager = \Drupal::service('theme.manager');
      $theme_initialization = \Drupal::service('theme.initialization');
      $default_theme = $config_factory->get('system.theme')->get('default');
      $current_active_theme = $theme_manager->getActiveTheme();
      if ($default_theme && $default_theme != $current_active_theme->getName()) {
        $theme_manager->setActiveTheme($theme_initialization->initTheme($default_theme));
      }

      try {
        $entityTypeManager = \Drupal::entityTypeManager();
        $view_builder = $entityTypeManager->getViewBuilder($this->entityTypeName);

        $preview = $view_builder->view($entity, $view_mode);
        $rendered = \Drupal::service('renderer');
        $html = $rendered->executeInRenderContext(
              new RenderContext(),
              function () use ($rendered, $preview) {
                  return $rendered->render($preview);
              }
          );
        $this->setPreviewHtml($html, $intent);
      } finally {
        // Revert the active theme, this is done inside a finally block so it is
        // executed even if an exception is thrown during rendering.
        if ($default_theme != $current_active_theme->getName()) {
          $theme_manager->setActiveTheme($current_active_theme);
        }
      }
      $intent->stopTimer('preview');
    }

    // Source URL.
    $intent->getOperation()->setSourceDeepLink($this->getViewUrl($entity), $intent->getActiveLanguage());

    // Fields.
    if ($entity instanceof FieldableEntityInterface) {
      $intent->startTimer('fields');
      /** @var \Drupal\Core\Entity\EntityFieldManagerInterface $entityFieldManager */
      $entityFieldManager = \Drupal::service('entity_field.manager');
      $type = $entity->getEntityTypeId();
      $bundle = $entity->bundle();
      $field_definitions = $entityFieldManager->getFieldDefinitions($type, $bundle);

      foreach ($field_definitions as $key => $field) {
        if ($intent->shouldIgnoreProperty($key)) {
          continue;
        }

        $handler = $this->flow->getController()->getFieldHandler($type, $bundle, $key);

        if (!$handler) {
          continue;
        }

        $handler->push($intent);
      }
      $intent->stopTimer('fields');
    }

    // Translations.
    if (!$intent->getActiveLanguage()
    && $this->isEntityTypeTranslatable($entity) && !$intent->isIndividualTranslation()) {
      $languages = array_keys($entity->getTranslationLanguages(FALSE));
      $allowed_languages = $this->flow->getController()->getAllowedLanguages();

      foreach ($languages as $language) {
        if (!empty($allowed_languages) && !in_array($language, $allowed_languages)) {
          continue;
        }

        $timer_name = 'translation-' . $language;
        $intent->startTimer($timer_name);

        $intent->changeTranslationLanguage($language);
        /**
         * @var \Drupal\Core\Entity\FieldableEntityInterface $translation
         */
        $translation = $entity->getTranslation($language);
        $this->push($intent, $translation);

        $intent->stopTimer($timer_name);
      }

      $intent->changeTranslationLanguage();
    }

    $intent->startTimer('before-push-event');
    // Allow other modules to extend the EntityHandlerBase push.
    // Dispatch entity push event.
    \Drupal::service('event_dispatcher')->dispatch(new BeforeEntityPush($entity, $intent), BeforeEntityPush::EVENT_NAME);
    $intent->stopTimer('before-push-event');

    return TRUE;
  }

  /**
   * Check if the entity should not be ignored from the push.
   *
   * @param \Drupal\cms_content_sync\PushIntent $intent
   *   The Sync Core Request.
   *
   * @throws \Exception
   *
   * @return bool
   *   Whether or not to ignore this push request
   */
  public function ignorePush(PushIntent $intent) {
    $reason = $intent->getReason();
    $action = $intent->getAction();

    if (PushIntent::PUSH_AUTOMATICALLY == $reason) {
      if (PushIntent::PUSH_MANUALLY == $this->settings['export']) {
        $intent->setIgnoreMessage('The entity is supposed to be pushed manually but was pushed automatically.');

        return TRUE;
      }
    }

    if (SyncIntent::ACTION_UPDATE == $action) {
      $status_proxy = new EntityStatusProxy(EntityStatus::getInfosForEntity($intent->getEntityType(), $intent->getUuid()));
      if ($status_proxy->isOverriddenLocally()) {
        $intent->setIgnoreMessage('The entity is overridden locally.');

        return TRUE;
      }
    }

    return FALSE;
  }

  /**
   * Whether or not menu item references should be pushed.
   *
   * @return bool
   *   Returns TRUE if the menu item should be pushed.
   */
  protected function pushReferencedMenuItems() {
    if (!isset($this->settings['handler_settings']['export_menu_items'])) {
      return TRUE;
    }

    return 0 !== $this->settings['handler_settings']['export_menu_items'];
  }

  /**
   * Check if the pull should be ignored.
   *
   * @return bool
   *   Whether or not to ignore this pull request
   */
  protected function ignorePull(PullIntent $intent) {
    $reason = $intent->getReason();
    $action = $intent->getAction();

    if (PullIntent::PULL_AUTOMATICALLY == $reason) {
      if (PullIntent::PULL_MANUALLY == $this->settings['import']) {
        // Once pulled manually, updates will arrive automatically.
        if ((PullIntent::PULL_AUTOMATICALLY != $reason || PullIntent::PULL_MANUALLY != $this->settings['import']) || SyncIntent::ACTION_CREATE == $action) {
          $intent->setIgnoreMessage('The entity is supposed to be pulled manually but was pulled automatically.');

          return TRUE;
        }
      }
    }

    if (SyncIntent::ACTION_UPDATE == $action) {
      $behavior = $this->settings['import_updates'];
      if (PullIntent::PULL_UPDATE_IGNORE == $behavior) {
        $intent->setIgnoreMessage('The Flow ignores entity updates.');

        return TRUE;
      }
    }

    if (SyncIntent::ACTION_DELETE != $action) {
      $entity_type = \Drupal::entityTypeManager()->getDefinition($intent->getEntityType());
      if ($entity_type->getKey('langcode')) {
        $langcode = $intent->getProperty($entity_type->getKey('langcode'));
        while (is_array($langcode)) {
          $langcode = reset($langcode);
        }

        if (!empty($langcode)) {
          if (!\Drupal::service('language_manager')->getLanguage($langcode)) {
            $intent->setIgnoreMessage('The language of this entity (' . $langcode . ') does not exist.');

            return TRUE;
          }

          $allowed_languages = $this->flow->getController()->getAllowedLanguages();
          if (!empty($allowed_languages) && !in_array($langcode, $allowed_languages)) {
            $intent->setIgnoreMessage('The language of this entity (' . $langcode . ') is not allowed.');

            return TRUE;
          }
        }
      }

      $entity = $intent->getEntity();
      if ($entity) {
        // If this is the default language, we may have untranslatable entity reference fields like
        // paragraph fields that still must be updated for a new revision. So this optimization is
        // not allowed for the default language.
        $new_version = $intent->getVersionId($this->isEntityTypeTranslatable($entity) && $entity->isDefaultTranslation());
        if ($intent->getSkipUnchanged() && $new_version && $entity) {
          $previous_version = $intent->getEntityStatus()->getTranslationData($entity->language()->getId(), EntityStatus::DATA_TRANSLATIONS_LAST_PULLED_VERSION_ID);
          if ($previous_version && $previous_version === $new_version) {
            $intent->setIgnoreMessage("The entity version hash didn't change.");

            return TRUE;
          }
        }
      }
    }

    return FALSE;
  }

  /**
   * Check whether the entity type supports having a label.
   *
   * @return bool
   *   Returns true if the entity type supports having a label.
   */
  protected function hasLabelProperty() {
    /**
     * @var \Drupal\Core\Entity\EntityTypeInterface $entity_type_entity
     */
    $entity_type_entity = \Drupal::service('entity_type.manager')
      ->getStorage($this->entityTypeName)
      ->getEntityType();

    return !!$entity_type_entity->getKey('label');
  }

  /**
   * Get the base entity properties that must be passed to the entities constructor.
   *
   * * @return array.
   */
  protected function getBaseEntityProperties(PullIntent $intent) {
    $entity_type = \Drupal::entityTypeManager()->getDefinition($intent->getEntityType());

    $base_data = [];

    if (EntityHandlerPluginManager::mapById($intent->getEntityType())) {
      $base_data['id'] = $intent->getId();
    }

    if ($this->hasLabelProperty() && $entity_type->getKey('label')) {
      $base_data[$entity_type->getKey('label')] = $intent->getOperation()->getName();
    }

    // Required as field collections share the same property for label and bundle.
    $base_data[$entity_type->getKey('bundle')] = $intent->getBundle();

    $base_data[$entity_type->getKey('uuid')] = $intent->getUuid();
    if ($entity_type->getKey('langcode')) {
      $base_data[$entity_type->getKey('langcode')] = $intent->getProperty($entity_type->getKey('langcode'));
      $default_langcode_key = $entity_type->getKey('default_langcode');
      if ($default_langcode_key) {
        $base_data[$default_langcode_key] = $intent->getProperty($default_langcode_key);
      }
    }

    // Set to unpublished by default?
    if ($this->entityTypeName === 'node') {
      $unpublished_revisions = PullIntent::PULL_UPDATE_UNPUBLISHED === $this->flow->getController()->getEntityTypeConfig($this->entityTypeName, $this->bundleName)['import_updates'];
      if ($unpublished_revisions) {
        $entity_type = \Drupal::entityTypeManager()->getDefinition($intent->getEntityType());
        $status_key = EntityHandlerPluginManager::getEntityTypeStatusProperty($entity_type, $intent->getBundle());
        if ($status_key) {
          $base_data[$status_key] = [['value' => 0]];
        }
      }
    }

    return $base_data;
  }

  /**
   * Create a new entity.
   *
   * @throws \Drupal\Component\Plugin\Exception\InvalidPluginDefinitionException
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   *
   * @return \Drupal\Core\Entity\EntityInterface
   *   Returns the created entity.
   */
  protected function createNew(PullIntent $intent) {
    $base_data = $this->getBaseEntityProperties($intent);

    $storage = \Drupal::entityTypeManager()->getStorage($intent->getEntityType());

    $entity_type = \Drupal::entityTypeManager()->getDefinition($intent->getEntityType());
    if ($entity_type->isTranslatable() && ($langcode_key = $entity_type->getKey('langcode')) && ($default_langcode_key = $entity_type->getKey('default_langcode'))) {
      $target_langcode = $base_data[$langcode_key];
      while (is_array($target_langcode)) {
        $target_langcode = array_values($target_langcode)[0];
      }
      $default_langcode = $base_data[$default_langcode_key];
      while (is_array($default_langcode)) {
        $default_langcode = array_values($default_langcode)[0];
      }
      $default_langcode = (bool) $default_langcode;

      $parent = $intent->getParent();
      if ($parent && $parent instanceof TranslatableInterface) {
        $parent = $parent->getUntranslated();
        $requires_translation_handling = $intent->getEntityType() === 'paragraph';
      }
      else {
        $requires_translation_handling = FALSE;
      }

      // Trying to create the translation before the default translation was created with the proper langcode.
      // This can create inconsistencies that are very hard to recover from e.g.
      // when you create paragraphs without a default langcode set it can break
      // the content and you will receive errors about duplicate UUIDs.
      if (!$default_langcode) {
        // So we create the entity in the default langcode and then translate it instead.
        $default_base_data = $base_data;
        $content_translation_source = $intent->getProperty('content_translation_source');
        if (empty($content_translation_source)) {
          throw new SyncException(SyncException::CODE_INVALID_PULL_REQUEST, NULL, 'The entity ' . json_encode($base_data) . ' is not provided in the default language but it also doesn\'t provide the translation source langcode so it can\'t be created because the default translation would be missing.');
        }
        while (is_array($content_translation_source)) {
          $content_translation_source = array_values($content_translation_source)[0];
        }
        $default_base_data[$langcode_key] = $content_translation_source;
        $default_base_data[$default_langcode_key] = [['value' => 1]];

        // Provide a way to identify the default translations that we created
        // automatically.
        if ($this->hasLabelProperty()) {
          $label_key = $entity_type->getKey('label');
          if ($label_key) {
            $default_base_data[$label_key] .= ' (' . $content_translation_source . ' default)';
          }
        }

        $status_key = EntityHandlerPluginManager::getEntityTypeStatusProperty($entity_type, $intent->getBundle());
        if ($status_key) {
          $default_base_data[$status_key] = [['value' => 0]];
        }

        $root_translation = $storage->create($default_base_data);

        // If only the translation of e.g. a paragraph was pushed, we still need
        // to create the paragrap in the default language or it will break the
        // paragraph.
        // If the default translation doesn't have any properities filled out, it
        // will throw an error because e.g. the status is missing. So we need to
        // set some default properties here.
        if ($root_translation instanceof FieldableEntityInterface) {
          $intent->setEntity($root_translation);
          $intent_language_before = $intent->getActiveLanguage();
          $intent->changeTranslationLanguage(NULL);
          if (!$this->setDefaultEntityValues($intent, $root_translation)) {
            $intent->setIgnoreMessage('Failed to set default entity values.');

            return NULL;
          }

          $entity = $root_translation->hasTranslation($target_langcode) ? $root_translation->getTranslation($target_langcode) : $root_translation->addTranslation($target_langcode, $base_data);
          $intent->changeTranslationLanguage($intent_language_before);
          return $entity;
        }

        return $root_translation->addTranslation($target_langcode, $base_data);
      }

      if ($default_langcode && $requires_translation_handling) {
        $base_data[$langcode_key] = $target_langcode;
        $base_data[$default_langcode_key] = [['value' => 1]];
        $root_translation = $storage->create($base_data);

        $intent->setEntity($root_translation);
        $intent_language_before = $intent->getActiveLanguage();
        $intent->changeTranslationLanguage(NULL);

        if (!$this->setDefaultEntityValues($intent, $root_translation, TRUE)) {
          $intent->setIgnoreMessage('Failed to set default entity values for translations.');

          return NULL;
        }

        $intent->changeTranslationLanguage($intent_language_before);

        return $root_translation;
      }
    }

    return $storage->create($base_data);
  }

  /**
   * Delete a entity.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity to delete.
   *
   * @throws \Drupal\cms_content_sync\Exception\SyncException
   *
   * @return bool
   *   Returns TRUE or FALSE for the deletion process
   */
  protected function deleteEntity(EntityInterface $entity) {
    try {
      $entity->delete();
    }
    catch (\Exception $e) {
      throw new SyncException(SyncException::CODE_ENTITY_API_FAILURE, $e);
    }

    return TRUE;
  }

  /**
   * Save an entity.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity to save.
   *
   * @throws \Drupal\Core\Entity\EntityStorageException
   */
  protected function saveEntity(EntityInterface $entity) {
    $entity->save();
  }

  /**
   * Set the values for the default language of the pulled entity even though it
   * wasn't provided. We fill out the necessary fields with the most reasonable
   * values until the real default language is also synchronized.
   *
   * @param \Drupal\cms_content_sync\PullIntent $intent
   *   The pulled entity.
   * @param \Drupal\Core\Entity\FieldableEntityInterface $entity
   *   The translation of the entity.
   * @param bool $keep_status
   *   Whether to keep the status as-is. Must be set when
   *   actually pulling the default language in the current request to avoid
   *   incorrectly unpublishing e.g. paragraphs.
   *
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   * @throws \Drupal\cms_content_sync\Exception\SyncException
   *
   * @return bool
   *   Returns TRUE when the values are set
   *
   * @see Flow::PULL_*
   */
  protected function setDefaultEntityValues(PullIntent $intent, ?FieldableEntityInterface $entity = NULL, $keep_status = FALSE) {
    /**
     * @var \Drupal\Core\Entity\TranslatableInterface $entity
     */
    if (!$entity) {
      $entity = $intent->getEntity()->getUntranslated();
    }

    if ($entity instanceof SynchronizableInterface) {
      $entity->setSyncing(TRUE);
    }

    /** @var \Drupal\Core\Entity\EntityFieldManagerInterface $entityFieldManager */
    $entityFieldManager = \Drupal::service('entity_field.manager');
    $type = $entity->getEntityTypeId();
    $bundle = $entity->bundle();
    $field_definitions = $entityFieldManager->getBaseFieldDefinitions($type, $bundle);

    $entity_type = \Drupal::entityTypeManager()->getDefinition($intent->getEntityType());
    $label = $entity_type->getKey('label');
    if ($label && !$intent->shouldMergeChanges() && $this->hasLabelProperty()) {
      $entity->set($label, $intent->getOperation()->getName($intent->getActiveLanguage()) . ' (default ' . $entity->language()->getId() . ')');
    }

    $user = \Drupal::currentUser();
    if (static::USER_PROPERTY && $entity->hasField(static::USER_PROPERTY) && !$intent->getEntityStatus()->isOverriddenLocally()) {
      $entity->set(static::USER_PROPERTY, [['target_id' => $user->id()]]);
    }
    if (static::USER_REVISION_PROPERTY && $entity->hasField(static::USER_REVISION_PROPERTY)) {
      $entity->set(static::USER_REVISION_PROPERTY, [['target_id' => $user->id()]]);
    }
    if (static::REVISION_TRANSLATION_AFFECTED_PROPERTY && $entity->hasField(static::REVISION_TRANSLATION_AFFECTED_PROPERTY)) {
      $entity->set(static::REVISION_TRANSLATION_AFFECTED_PROPERTY, 1);
    }

    $status_property = EntityHandlerPluginManager::getEntityTypeStatusProperty($entity_type, $entity->bundle());
    $status_before = $status_property ? $intent->getProperty($status_property) : NULL;
    // Set to unpublished by default if the entity has a status property.
    if (!empty($status_before)) {
      $intent->overwriteProperty($status_property, [['value' => 0]]);
    }

    if ($entity->isDefaultTranslation()) {
      $intent->startTimer('default-fields');
      foreach ($field_definitions as $key => $field) {
        if ($intent->shouldIgnoreProperty($key)) {
          continue;
        }

        $handler = $this->flow->getController()->getFieldHandler($type, $bundle, $key);

        if (!$handler) {
          continue;
        }

        // These were already overwritten at the ->createNew() method, so we can
        // safely ignore them here.
        if ($key === $entity_type->getKey('langcode') || $key === $entity_type->getKey('default_langcode') || $key === 'content_translation_source' || $key === $status_property) {
          continue;
        }

        $handler->pull($intent);
      }
      $intent->stopTimer('default-fields');
    }

    if (!empty($status_before)) {
      if (!$keep_status) {
        $entity->set($status_property, [['value' => 0]]);
      }
      $intent->overwriteProperty($status_property, $status_before);
    }

    // In case we create the default revision like this, we need to save it first
    // otherwise for paragraphs it will throw an error about the status being null
    // because paragraphs can't handle the entity being only saved as a translation
    // the first time.
    if ($entity->isDefaultTranslation()) {
      $parent = $intent->getParent();
      if ($parent && $parent instanceof TranslatableInterface) {
        $parent = $parent->getUntranslated();
      }
      $field = $intent->getProperty('parent_field_name');
      if ($entity instanceof ParagraphInterface && $parent && $field) {
        $entity->setParentEntity($parent, $field);
      }

      $intent->startTimer('default-save');
      $entity->save();
      $intent->stopTimer('default-save');

      // For paragraphs, we need to add all translations immediately and set
      // them to unpublished. Otherwise the new paragraph will show up in the
      // translations with the default language's content.
      if ($entity instanceof ParagraphInterface && $parent && $parent instanceof TranslatableInterface) {
        foreach ($parent->getTranslationLanguages(FALSE) as $language) {
          $langcode = $language->getId();

          $translated = $entity->hasTranslation($langcode) ? $entity->getTranslation($langcode) : $entity->addTranslation($langcode);
          $this->setDefaultEntityValues($intent, $translated);
        }
      }
    }

    if ($entity instanceof SynchronizableInterface) {
      $entity->setSyncing(FALSE);
    }

    return TRUE;
  }

  /**
   * Set the values for the pulled entity.
   *
   * @param \Drupal\cms_content_sync\PullIntent $intent
   *   The pulled entity.
   * @param \Drupal\Core\Entity\FieldableEntityInterface $entity
   *   The translation of the entity.
   *
   * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException
   * @throws \Drupal\cms_content_sync\Exception\SyncException
   *
   * @return bool
   *   Returns TRUE when the values are set
   *
   * @see Flow::PULL_*
   */
  protected function setEntityValues(PullIntent $intent, ?FieldableEntityInterface $entity = NULL) {
    if (!$entity) {
      $entity = $intent->getEntity();
    }

    if ($entity instanceof SynchronizableInterface) {
      $entity->setSyncing(TRUE);
    }

    /** @var \Drupal\Core\Entity\EntityFieldManagerInterface $entityFieldManager */
    $entityFieldManager = \Drupal::service('entity_field.manager');
    $type = $entity->getEntityTypeId();
    $bundle = $entity->bundle();
    $field_definitions = $entityFieldManager->getFieldDefinitions($type, $bundle);

    $entity_type = \Drupal::entityTypeManager()->getDefinition($intent->getEntityType());
    $label = $entity_type->getKey('label');
    if ($label && !$intent->shouldMergeChanges() && $this->hasLabelProperty()) {
      $entity->set($label, $intent->getOperation()->getName($intent->getActiveLanguage()));
    }

    $static_fields = $this->getStaticFields();

    $unpublished_revisions = PullIntent::PULL_UPDATE_UNPUBLISHED === $this->flow->getController()->getEntityTypeConfig($this->entityTypeName, $this->bundleName)['import_updates'];

    $is_translatable = $this->isEntityTypeTranslatable($entity);
    $is_translation = $is_translatable && !$entity->isDefaultTranslation();
    $skip_not_translatable_fields = !$intent->isIndividualTranslation() && $is_translatable && $is_translation;
    $skip_save = !$intent->isIndividualTranslation() && $is_translatable && $is_translation && !$unpublished_revisions;

    $user = \Drupal::currentUser();
    if (static::USER_PROPERTY && $entity->hasField(static::USER_PROPERTY) && !$intent->getEntityStatus()->isOverriddenLocally()) {
      $entity->set(static::USER_PROPERTY, [['target_id' => $user->id()]]);
    }
    if (static::USER_REVISION_PROPERTY && $entity->hasField(static::USER_REVISION_PROPERTY)) {
      $entity->set(static::USER_REVISION_PROPERTY, [['target_id' => $user->id()]]);
    }
    if (static::REVISION_TRANSLATION_AFFECTED_PROPERTY && $entity->hasField(static::REVISION_TRANSLATION_AFFECTED_PROPERTY)) {
      $entity->set(static::REVISION_TRANSLATION_AFFECTED_PROPERTY, 1);
    }

    $is_update = SyncIntent::ACTION_UPDATE === $intent->getAction() && !$intent->getIsNewTranslation();

    $intent->startTimer('fields');
    foreach ($field_definitions as $key => $field) {
      if ($intent->shouldIgnoreProperty($key)) {
        continue;
      }

      $handler = $this->flow->getController()->getFieldHandler($type, $bundle, $key);

      if (!$handler) {
        continue;
      }

      // This field cannot be updated.
      if (in_array($key, $static_fields) && $is_update) {
        continue;
      }

      if ($skip_not_translatable_fields && !$field->isTranslatable()) {
        continue;
      }

      if ('image' == $field->getType() || 'file' == $field->getType()) {
        // Focal Point takes information from the image field directly
        // so we have to set it before the entity is saved the first time.
        $data = $intent->getProperty($key);
        if (NULL === $data) {
          $data = [];
        }
        foreach ($data as &$value) {
          /**
           * @var \Drupal\file\Entity\File $file
           */
          $file = $intent->loadEmbeddedEntity($value);
          if ($file) {
            if ('image' == $field->getType()) {
              $moduleHandler = \Drupal::service('module_handler');
              if ($moduleHandler->moduleExists('crop') && $moduleHandler->moduleExists('focal_point')) {
                /**
                 * @var \Drupal\crop\Entity\Crop $crop
                 */
                $crop = Crop::findCrop($file->getFileUri(), 'focal_point');
                if ($crop) {
                  $position = $crop->position();

                  // Convert absolute to relative.
                  $size = getimagesize($file->getFileUri());
                  if (empty($size[0]) || empty($size[1])) {
                    $this->logger->warning(
                      'Can\'t push crop entity @crop_id: File @uri doesn\'t exist in the file system, the file permissions forbid access or it\'s not a valid non-vector image.<br>Flow: @flow_id | Pool: @pool_id',
                      [
                        '@crop_id' => $crop->id(),
                        '@uri' => $file->getFileUri(),
                        '@flow_id' => $intent->getFlow()->id(),
                        '@pool_id' => implode(',', $intent->getPoolIds()),
                      ]
                    );
                  }
                  else {
                    $value['focal_point'] = ($position['x'] / $size[0] * 100) . ',' . ($position['y'] / $size[1] * 100);
                  }
                }
              }
            }
          }
        }

        $intent->overwriteProperty($key, $data);
      }

      $handler->pull($intent);
    }
    $intent->stopTimer('fields');

    if ($unpublished_revisions) {
      if ($entity instanceof NodeInterface) {
        if ($entity->id()) {
          if ($intent->getIsNewTranslation()) {
            $entity->set('status', 0);
          }
          else {
            $entity->isDefaultRevision(FALSE);
          }
        }
        else {
          $entity->setUnpublished();
        }
      }
    }

    if (!$intent->getActiveLanguage()) {
      $created = $this->getDateProperty($intent, 'created');
      // See https://www.drupal.org/project/drupal/issues/2833378
      if ($created && method_exists($entity, 'getCreatedTime') && method_exists($entity, 'setCreatedTime')) {
        if ($created !== $entity->getCreatedTime()) {
          $entity->setCreatedTime($created);
        }
      }
      if ($entity instanceof EntityChangedInterface) {
        $entity->setChangedTime(time());
      }
    }

    if ($is_translatable && !$intent->getActiveLanguage()) {
      $this->pullTranslations($intent, $entity);
    }

    if (!$skip_save) {
      try {
        $intent->startTimer('save');
        $this->saveEntity($entity, $intent);
        if ($unpublished_revisions) {
          if ($bundle_entity_type = $entity->getEntityType()->getBundleEntityType()) {
            $bundle_entity_type = \Drupal::entityTypeManager()->getStorage($bundle_entity_type)->load($entity->bundle());
            if (($bundle_entity_type instanceof RevisionableEntityBundleInterface && $bundle_entity_type->shouldCreateNewRevision()) || 'paragraph' == $entity->getEntityTypeId()) {
              $entity->setNewRevision(TRUE);
              $entity->setRevisionTranslationAffected(FALSE);
            }
          }
        }
        $intent->stopTimer('save');
      }
      catch (\Exception $e) {
        throw new SyncException(SyncException::CODE_ENTITY_API_FAILURE, $e);
      }
    }

    if ($entity instanceof SynchronizableInterface) {
      $entity->setSyncing(FALSE);
    }

    return TRUE;
  }

  /**
   * Pull the translations. This will process translations that are embedded
   * in the request but also delete translations that are no longer present.
   *
   * @return bool whether anything was updated
   */
  protected function pullTranslations(PullIntent $intent, EntityInterface $entity) {
    $changed = FALSE;

    $languages = $intent->getTranslationLanguages();

    if (!$intent->isIndividualTranslation()) {
      $allowed_languages = $this->flow->getController()->getAllowedLanguages();
      foreach ($languages as $language) {
        if (!\Drupal::service('language_manager')->getLanguage($language)) {
          continue;
        }

        if (!empty($allowed_languages) && !in_array($language, $allowed_languages)) {
          continue;
        }

        // Skip original translation that was already processed outside of this method.
        if ($entity->getUntranslated()->language()->getId() === $language) {
          continue;
        }

        if ($entity instanceof NodeInterface) {
          if (!$entity->wasDefaultRevision() || !$entity->isDefaultRevision()) {
            $entity = $intent->getEntity(TRUE);
            if ($entity instanceof NodeInterface) {
              $entity->isDefaultRevision(TRUE);
              $entity->isRevisionTranslationAffected(FALSE);
            }
          }
        }

        $timer_name = 'translation-' . $language;
        $intent->startTimer($timer_name);

        /**
         * If the provided entity is fieldable, translations are as well.
         *
         * @var \Drupal\Core\Entity\FieldableEntityInterface $translation
         */
        if ($entity->hasTranslation($language)) {
          $translation = $entity->getTranslation($language);
        }
        else {
          $translation = $entity->addTranslation($language);
          $intent->setIsNewTranslation(TRUE);
        }

        $translation->setRevisionTranslationAffected(TRUE);

        $intent->changeTranslationLanguage($language);
        if (!$this->ignorePull($intent)) {
          if ($this->setEntityValues($intent, $translation)) {
            $changed = TRUE;
            $new_version = $intent->getVersionId($entity->isDefaultTranslation());
            if ($new_version) {
              $intent->getEntityStatus()->setTranslationData($language, EntityStatus::DATA_TRANSLATIONS_LAST_PULLED_VERSION_ID, $new_version);
            }
          }
        }
        $intent->setIsNewTranslation(FALSE);

        $intent->stopTimer($timer_name);
      }
    }

    // Delete translations that were deleted on master site.
    // If the entity was pulled embedded we have to be more careful as we
    // might run into timing issues- if users pull an older version of an
    // embedded entity we don't want to delete it's translations.
    if (boolval($this->settings['import_deletion_settings']['import_deletion']) && !$intent->wasPulledEmbedded()) {
      $existing = $entity->getTranslationLanguages(FALSE);
      $remove = [];
      foreach ($existing as $language) {
        if (!in_array($language->getId(), $languages) && $language->getId() !== $entity->language()->getId() && !!$intent->getEntityStatus()->getLastPull($language->getId())) {
          $remove[] = $language->getId();
        }
      }

      if (count($remove)) {
        $changed = TRUE;

        foreach ($remove as $language) {
          $entity->removeTranslation($language);
        }

        try {
          $this->saveEntity($entity, $intent);
        }
        catch (\Exception $e) {
          throw new SyncException(SyncException::CODE_ENTITY_API_FAILURE, $e);
        }
      }
    }

    if (!$intent->isIndividualTranslation()) {
      $intent->changeTranslationLanguage();
    }

    return $changed;
  }

  /**
   * Set the date property.
   */
  protected function setDateProperty(SyncIntent $intent, string $name, int $timestamp) {
    $intent->setProperty($name, ['value' => $timestamp]);
  }

  /**
   * Get the date property.
   */
  protected function getDateProperty(SyncIntent $intent, string $name) {
    $value = $intent->getProperty($name);
    if (is_array($value)) {
      return $value[0]['value'] ?? $value['value'];
    }

    return (int) $value;
  }

  /**
   * Get a list of fields that can't be updated.
   */
  protected function getStaticFields() {
    return [];
  }

  /**
   * Get the entity name.
   */
  protected function getEntityName(EntityInterface $entity, PushIntent $intent) {
    return $entity->label();
  }

  /**
   * Set the preview html content.
   */
  protected function setPreviewHtml($html, PushIntent $intent) {
    if ($html) {
      // Turn relative URLs into absolute URLs using the current site's
      // base URL.
      // For images to show up in the pull dashboard the webserver may
      // still have to be configured to allow cross-origin embeds of files
      // i.e. from https://embed.content-sync.io
      $base_url = ContentSyncSettings::getInstance()->getSiteBaseUrl();
      $parts = parse_url($base_url);
      $base_url_without_path = $parts['scheme'] . '://' . $parts['host'] . '/';
      $html = preg_replace_callback('@ (src|href)=("|\')/([^\'"]+)("|\')@', function ($matches) use ($base_url_without_path) {
          [$match, $attribute, $quote1, $path, $quote2] = $matches;

          return ' ' . $attribute . '=' . $quote1 . $base_url_without_path . $path . $quote2;
      }, $html);
    }

    try {
      $intent->getOperation()->setPreviewHtml($html, $intent->getActiveLanguage());
    }
    catch (\Exception $error) {
      $entity = $intent->getEntity();

      $messenger = \Drupal::messenger();
      $messenger->addWarning(
            t(
                'Failed to save preview for %label: %error',
                [
                  '%error' => $error->getMessage(),
                  '%label' => $entity->label(),
                ]
            )
            );

      $this->logger->error('Failed to save preview when pushing @type.@bundle @id @label: @error', [
        '@type' => $entity->getEntityTypeId(),
        '@bundle' => $entity->bundle(),
        '@id' => $entity->id(),
        '@label' => $entity->label(),
        '@error' => $error->getMessage(),
      ]);
    }
  }

  /**
   * Check if the entity type is translatable.
   *
   * @param \Drupal\Core\Entity\EntityInterface $entity
   *   The entity to check for.
   *
   * @return bool
   *   Returns true if the entity is translatable.
   */
  protected function isEntityTypeTranslatable(EntityInterface $entity) {
    return $entity instanceof TranslatableInterface && $entity->getEntityType()->getKey('langcode');
  }

  /**
   * {@inheritDoc}
   */
  public function getSyncCoreList(Flow $flow, RemoteRequestQueryParamsEntityList $queryObject) {
    $entity_type = $queryObject->getNamespaceMachineName();
    $bundle = $queryObject->getMachineName();

    $page = (int) $queryObject->getPage();
    if (!$page) {
      $page = 0;
    }

    $items_per_page = (int) $queryObject->getItemsPerPage();
    if (!$items_per_page) {
      $items_per_page = 0;
    }

    // Need to convert miliseconds to seconds.
    $changed_after = $queryObject->getChangedAfter() ? floor((int) $queryObject->getChangedAfter() / 1_000) : NULL;

    $query = \Drupal::request()->query->all();
    $search = empty($_GET['search']) ? NULL : $query['search'];

    $skip = $page * $items_per_page;

    $database = \Drupal::database();

    $entity_type_storage = \Drupal::entityTypeManager()->getStorage($entity_type);
    $bundle_key = $entity_type_storage->getEntityType()->getKey('bundle');
    $id_key = $entity_type_storage->getEntityType()->getKey('id');
    $langcode_key = $entity_type_storage->getEntityType()->getKey('langcode');
    if ($entity_type_storage instanceof SqlContentEntityStorage) {
      $base_table = $entity_type_storage->getBaseTable();
      $data_table = $entity_type_storage->getDataTable();
      $definitions = \Drupal::service('entity_field.manager')->getFieldDefinitions($entity_type, $bundle);
      $prefix = '';
    }
    elseif ($entity_type_storage instanceof ConfigEntityStorage) {
      $base_table = 'config';
      $definitions = [];
      $prefix = $entity_type_storage->getEntityType()->getConfigPrefix() . '.';
    }
    else {
      throw new \Exception('Entity type ' . $entity_type . ' uses unknown storage.');
    }

    $query = $database->select($base_table, 'bt');
    if ($entity_type_storage instanceof ConfigEntityStorage) {
      $query
        ->fields('bt', ['name']);
      $query
        ->condition('name', $query->escapeLike($prefix) . '%', 'LIKE');
    }
    else {
      if (!empty($bundle_key)) {
        $query
          ->condition('bt.' . $bundle_key, $bundle);
      }
      $query
        ->fields('bt', [$id_key]);
    }

    // Join data table if the entity has one (files for example have none).
    if (!empty($data_table)) {
      $query->join($data_table, 'dt', 'dt.' . $id_key . '=bt.' . $id_key . ($langcode_key ? ' AND dt.' . $langcode_key . '=bt.' . $langcode_key : ''));
    }
    $property_table_prefix = empty($data_table) ? 'bt' : 'dt';

    $label_property = $entity_type_storage->getEntityType()->getKey('label');
    if (!empty($search) && !empty($label_property)) {
      $query
        ->condition($property_table_prefix . '.' . $label_property, '%' . $database->escapeLike($search) . '%', 'LIKE');
    }

    // Ignore unpublished entities based on the flow configuration.
    $entity_type_config = $flow->getController()->getEntityTypeConfig($entity_type, $bundle);
    $handler_settings = $entity_type_config['handler_settings'];
    $status_key = 'config' === $base_table ? NULL : EntityHandlerPluginManager::getEntityTypeStatusProperty($entity_type_storage->getEntityType(), $bundle);
    if (TRUE == $handler_settings['ignore_unpublished'] && !empty($status_key)) {
      if (TRUE == $handler_settings['allow_explicit_unpublishing']) {
        // Join the entity status table to check if the entity has been exported before to allow explizit unpublishing.
        $query->leftJoin('cms_content_sync_entity_status', 'cses', 'cses.entity_uuid = bt.uuid');

        // If status is 0 and the entity has been exported before.
        $and = $query->andConditionGroup()
          ->condition($property_table_prefix . '.' . $status_key, '0')
          ->condition('cses.last_export', 0, '>');

        $or = $query->orConditionGroup()
          ->condition($and)
          ->condition($property_table_prefix . '.' . $status_key, '1');

        $query->condition($or);
      }
      else {
        $query
          ->condition($property_table_prefix . '.' . $status_key, '1');
      }
    }

    if (!empty($definitions['created'])) {
      if ($changed_after) {
        $query
          ->condition($property_table_prefix . '.created', $changed_after, '>');
      }
      $query
        ->orderBy($property_table_prefix . '.created', 'ASC');
    }
    elseif ('config' === $base_table) {
      $query->orderBy('bt.name', 'ASC');
    }
    else {
      $query->orderBy('bt.' . $id_key, 'ASC');
    }
    $total_number_of_items = (int) $query->countQuery()->execute()->fetchField();

    $items = [];

    if ($total_number_of_items && $items_per_page) {
      $ids = $query
        ->range($skip, $items_per_page)
        ->execute()
        ->fetchAll(\PDO::FETCH_COLUMN);
      if ($prefix) {
        foreach ($ids as $i => $id) {
          $ids[$i] = substr($id, strlen($prefix));
        }
      }
      $entities = $entity_type_storage->loadMultiple($ids);
      foreach ($entities as $entity) {
        $items[] = $this->getSyncCoreListItem($flow, $entity, EntityStatus::getInfosForEntity($entity->getEntityTypeId(), $entity->uuid(), ['flow' => $flow->id()]));
      }
    }

    if (!$items_per_page) {
      $number_of_pages = $total_number_of_items;
    }
    else {
      $number_of_pages = ceil($total_number_of_items / $items_per_page);
    }

    $result = new RemoteEntityListResponse();
    $result->setPage($page);
    $result->setNumberOfPages($number_of_pages);
    $result->setItemsPerPage($items_per_page);
    $result->setTotalNumberOfItems($total_number_of_items);
    $result->setItems($items);

    return $result;
  }

  /**
   * {@inheritDoc}
   */
  public function getSyncCoreListItem(?Flow $flow, ?object $entity, array $statuses) {
    $item = new RemoteEntitySummary();

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

    $pools = [];
    $last_push = NULL;
    foreach ($statuses as $status) {
      $pools[] = $status->get('pool')->value;
      if ($status->getLastPush($language)) {
        if (!$last_push || $status->getLastPush($language) > $last_push->getLastPush($language)) {
          $last_push = $status;
        }
      }
    }

    $item->setPoolMachineNames($pools);

    $item->setIsSource(empty($statuses) || (bool) $last_push);

    if ($entity) {
      $item->setEntityTypeNamespaceMachineName($entity->getEntityTypeId());
      $item->setEntityTypeMachineName($entity->bundle());
      $item->setEntityTypeVersion(Flow::getEntityTypeVersion($entity->getEntityTypeId(), $entity->bundle()));
      $item->setRemoteUuid($entity->uuid());
      if (EntityHandlerPluginManager::mapById($entity->getEntityTypeId())) {
        $item->setRemoteUniqueId($entity->id());
      }
      $item->setLanguage($entity->language()->getId());
      $item->setName($entity->label());

      $item->setIsDeleted(FALSE);

      if ($flow) {
        $config = $flow->getController()->getEntityTypeConfig($entity->getEntityTypeId(), $entity->bundle());
        $handler = $flow->getController()->getEntityTypeHandler($entity->getEntityTypeId(), $entity->bundle(), $config);
      }
      else {
        $entity_plugin_manager = \Drupal::service('plugin.manager.cms_content_sync_entity_handler');
        $entity_handlers = $entity_plugin_manager->getHandlerOptions($entity->getEntityTypeId(), $entity->bundle(), TRUE);
        $entity_handler_names = array_keys($entity_handlers);
        $handler_id = reset($entity_handler_names);
        $handler = $entity_plugin_manager->createInstance($handler_id, [
          'entity_type_name' => $entity->getEntityTypeId(),
          'bundle_name' => $entity->bundle(),
          'settings' => [],
          'sync' => NULL,
        ]);
      }
      $item->setViewUrl($handler->getViewUrl($entity));

      if ($flow && $entity instanceof TranslatableInterface) {
        $translations = [];
        $config = $flow->getController()->getEntityTypeConfig($entity->getEntityTypeId(), $entity->bundle());
        $handler = $flow->getController()->getEntityTypeHandler($entity->getEntityTypeId(), $entity->bundle(), $config);
        foreach ($entity->getTranslationLanguages(FALSE) as $language) {
          $translation_dto = new RemoteEntityTranslationDetails();
          $translation_dto->setLanguage($language->getId());
          $view_url = $handler->getViewUrl($entity->getTranslation($language->getId()));
          $translation_dto->setViewUrl($view_url);
          $translations[] = $translation_dto;
        }
        $item->setTranslations($translations);
      }
    }
    else {
      $status = $last_push ? $last_push : reset($statuses);
      $item->setEntityTypeNamespaceMachineName($status->getEntityTypeName());
      $item->setEntityTypeMachineName('*');
      $item->setLanguage('*');
      $item->setName('?');
      $item->setEntityTypeVersion($status->getEntityTypeVersion());
      $item->setRemoteUuid($status->getUuid());

      $item->setIsDeleted($status->isDeleted());
    }

    return $item;
  }

}

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

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