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;
}
}
