View source
<?php
namespace Drupal\acquia_contenthub;
use Drupal\acquia_contenthub\Client\ClientManagerInterface;
use Drupal\acquia_contenthub\QueueItem\ImportQueueItem;
use Drupal\acquia_contenthub\Session\ContentHubUserSession;
use Drupal\Component\Uuid\Uuid;
use Drupal\Core\Database\Connection;
use Drupal\Core\Entity\ContentEntityInterface;
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Entity\EntityRepositoryInterface;
use Drupal\Core\Language\LanguageManagerInterface;
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
use Drupal\Core\Queue\QueueFactory;
use Drupal\Core\StringTranslation\StringTranslationTrait;
use Drupal\Core\StringTranslation\TranslationInterface;
use Drupal\Core\Url;
use Drupal\diff\DiffEntityComparison;
use Symfony\Component\DependencyInjection\ContainerInterface;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
use Symfony\Component\Serializer\SerializerInterface;
class ImportEntityManager {
use StringTranslationTrait;
private $format = 'acquia_contenthub_cdf';
private $database;
private $loggerFactory;
private $serializer;
private $entityRepository;
private $clientManager;
private $contentHubEntitiesTracking;
private $diffEntityComparison;
private $entityManager;
private $queue;
protected $languageManager;
public static function create(ContainerInterface $container) {
return new static($container
->get('database'), $container
->get('logger.factory'), $container
->get('serializer'), $container
->get('entity.repository'), $container
->get('acquia_contenthub.client_manager'), $container
->get('acquia_contenthub.acquia_contenthub_entities_tracking'), $container
->get('diff.entity_comparison'), $container
->get('acquia_contenthub.entity_manager'), $container
->get('string_translation'), $container
->get('queue'), $container
->get('language_manager'));
}
public function __construct(Connection $database, LoggerChannelFactoryInterface $logger_factory, SerializerInterface $serializer, EntityRepositoryInterface $entity_repository, ClientManagerInterface $client_manager, ContentHubEntitiesTracking $entities_tracking, DiffEntityComparison $entity_comparison, EntityManager $entity_manager, TranslationInterface $string_translation, QueueFactory $queue_factory, LanguageManagerInterface $language_manager) {
$this->database = $database;
$this->loggerFactory = $logger_factory;
$this->serializer = $serializer;
$this->entityRepository = $entity_repository;
$this->clientManager = $client_manager;
$this->contentHubEntitiesTracking = $entities_tracking;
$this->diffEntityComparison = $entity_comparison;
$this->entityManager = $entity_manager;
$this->stringTranslation = $string_translation;
$this->queue = $queue_factory;
$this->languageManager = $language_manager;
}
private function compareReferencedEntities(EntityInterface $entity) {
$new_references = $entity
->referencedEntities();
$old_references = $entity->original
->referencedEntities();
$new_uuids = array_map([
$this,
'excludeCompareReferencedEntities',
], $new_references);
$old_uuids = array_map([
$this,
'excludeCompareReferencedEntities',
], $old_references);
$changes = array_diff($new_uuids, $old_uuids);
if (!empty($changes)) {
return TRUE;
}
return FALSE;
}
private function excludeCompareReferencedEntities(EntityInterface $entity) {
if ($entity
->getEntityType()
->entityClassImplements('\\Drupal\\Core\\Config\\Entity\\ConfigEntityInterface')) {
return FALSE;
}
return $entity
->uuid();
}
private function compareRevisions(EntityInterface $entity) {
$field_comparisons = $this->diffEntityComparison
->compareRevisions($entity->original, $entity);
foreach ($field_comparisons as $field_comparison => $field_comparison_value) {
list($entity_id, $field_comparison_data) = explode(':', $field_comparison);
list($entity_type_id, $field_comparison_name) = explode('.', $field_comparison_data);
if ($entity_id == $entity
->id() && $entity_type_id == $entity
->getEntityTypeId() && $this
->isFieldReferencedToSubclassOf($entity, $field_comparison_name)) {
continue;
}
if ($field_comparison_value['#data']['#left'] !== $field_comparison_value['#data']['#right']) {
return TRUE;
}
}
return FALSE;
}
private function isFieldReferencedToSubclassOf(EntityInterface $entity, $field_name, $subclass = '\\Drupal\\Core\\Config\\Entity\\ConfigEntityInterface') {
if (empty($entity) || empty($field_name)) {
return FALSE;
}
$field_type = $entity
->getFieldDefinition($field_name)
->getType();
if ($field_type == 'entity_reference') {
$field_references = $entity
->get($field_name)
->referencedEntities();
foreach ($field_references as $field_reference) {
if ($field_reference
->getEntityType()
->entityClassImplements($subclass)) {
return TRUE;
}
}
}
return FALSE;
}
public function entityPresave(EntityInterface $entity) {
if (!isset($entity->original) || !empty($entity->__contenthub_entity_syncing)) {
return;
}
$imported_entity = $this
->findRootAncestorImportEntity($entity);
if (!$imported_entity || $imported_entity
->isPendingSync() || $imported_entity
->hasLocalChange()) {
return;
}
$has_local_change = $this
->compareRevisions($entity) || $this
->compareReferencedEntities($entity);
if (!$has_local_change) {
return;
}
$imported_entity
->setLocalChange();
$imported_entity
->save();
}
private function loadRemoteEntity($uuid) {
$entity = $this->clientManager
->createRequest('readEntity', [
$uuid,
]);
if (!$entity) {
return FALSE;
}
return new ContentHubEntityDependency($entity);
}
private function getAllRemoteDependencies(ContentHubEntityDependency $content_hub_entity, array &$dependencies, $use_chain = TRUE) {
$dep_dependencies = $this
->getRemoteDependencies($content_hub_entity, $use_chain);
foreach ($dep_dependencies as $uuid => $content_hub_dependency) {
if (isset($dependencies[$uuid])) {
continue;
}
$imported_entity = $this->contentHubEntitiesTracking
->loadImportedByUuid($uuid);
if ($imported_entity && !$imported_entity
->isDependent() && $imported_entity
->getModified() === $content_hub_dependency
->getRawEntity()
->getModified()) {
continue;
}
$dependencies[$uuid] = $content_hub_dependency;
$this
->getAllRemoteDependencies($content_hub_dependency, $dependencies, $use_chain);
}
return array_reverse($dependencies, TRUE);
}
private function getRemoteDependencies(ContentHubEntityDependency $content_hub_entity, $use_chain = TRUE) {
$dependencies = [];
$uuids = $content_hub_entity
->getRemoteDependencies();
foreach ($uuids as $uuid) {
$content_hub_dependent_entity = $this
->loadRemoteEntity($uuid);
if ($content_hub_dependent_entity === FALSE) {
continue;
}
if ($content_hub_entity
->isInDependencyChain($content_hub_dependent_entity) && $use_chain) {
$content_hub_dependent_entity
->setParent($content_hub_entity);
continue;
}
$content_hub_dependent_entity
->setParent($content_hub_entity);
$dependencies[$uuid] = $content_hub_dependent_entity;
}
return $dependencies;
}
public function import($uuid, $include_dependencies = TRUE, $author = NULL, $status = 0) {
if (\Drupal::config('acquia_contenthub.entity_config')
->get('import_with_queue')) {
return $this
->addEntityToImportQueue($uuid, $include_dependencies, $author, $status);
}
return $this
->importRemoteEntity($uuid, $include_dependencies, $author, $status);
}
public function importRemoteEntity($uuid, $include_dependencies = TRUE, $author = NULL, $status = 0) {
if (!Uuid::isValid($uuid)) {
throw new AccessDeniedHttpException();
}
$contenthub_entity = $this
->loadRemoteEntity($uuid);
if (!$contenthub_entity) {
$message = $this
->t('Entity with UUID = @uuid not found.', [
'@uuid' => $uuid,
]);
return $this
->jsonErrorResponseMessage($message, FALSE, 404);
}
$origin = $contenthub_entity
->getRawEntity()
->getOrigin();
$site_origin = $this->contentHubEntitiesTracking
->getSiteOrigin();
if ($origin === $site_origin) {
$args = [
'@type' => $contenthub_entity
->getRawEntity()
->getType(),
'@uuid' => $contenthub_entity
->getRawEntity()
->getUuid(),
'@origin' => $origin,
];
$message = $this
->t('Cannot save "@type" entity with uuid="@uuid". It has the same origin as this site: "@origin"', $args);
$this->loggerFactory
->get('acquia_contenthub')
->warning($message);
$result = FALSE;
return $this
->jsonErrorResponseMessage($message, $result, 403);
}
$allowed_entity_types = $this->entityManager
->getAllowedEntityTypes();
$contenthub_entity_attribute = $contenthub_entity
->getRawEntity()
->getAttribute('type')['value'] ?? NULL;
$contenthub_entity_bundle = $contenthub_entity_attribute ? reset($contenthub_entity_attribute) : NULL;
if ($contenthub_entity_bundle && !array_key_exists($contenthub_entity_bundle, $allowed_entity_types[$contenthub_entity
->getRawEntity()
->getType()])) {
$args = [
'@type' => $contenthub_entity
->getRawEntity()
->getType(),
'@uuid' => $contenthub_entity
->getRawEntity()
->getUuid(),
'@bundle' => $contenthub_entity_bundle,
];
$message = $this
->t('Cannot save "@type" entity with uuid="@uuid". Missing "@type" entity with bundle "@bundle"', $args);
$this->loggerFactory
->get('acquia_contenthub')
->warning($message);
$result = FALSE;
return $this
->jsonErrorResponseMessage($message, $result, 403);
}
if (!$this
->verifyLanguageSupportability($contenthub_entity)) {
$args = [
'@type' => $contenthub_entity
->getRawEntity()
->getType(),
'@uuid' => $contenthub_entity
->getRawEntity()
->getUuid(),
];
$message = $this
->t('Cannot save "@type" entity with uuid="@uuid". The site does not support any of the languages available for this entity.', $args);
$this->loggerFactory
->get('acquia_contenthub')
->warning($message);
return $this
->jsonErrorResponseMessage($message, FALSE, 403);
}
$dependencies = [];
if ($include_dependencies) {
$dependencies = $this
->getAllRemoteDependencies($contenthub_entity, $dependencies, TRUE);
}
$contenthub_entity
->setStatus($status);
$contenthub_entity
->setAuthor($author);
foreach ($dependencies as $uuid => $dependency) {
$dependencies[$uuid]
->setAuthor($author);
$entity_type = $dependency
->getEntityType();
if (isset($status) && $entity_type === 'node' && !$this->contentHubEntitiesTracking
->loadImportedByUuid($uuid)) {
$dependencies[$uuid]
->setStatus($status);
}
}
return $this
->importRemoteEntityDependencies($contenthub_entity, $dependencies);
}
public function verifyLanguageSupportability(ContentHubEntityDependency $contenthub_entity_dependency) {
$contenthub_entity = $contenthub_entity_dependency
->getRawEntity();
$entity_type = $contenthub_entity
->getType();
$attributes = $contenthub_entity
->getAttributes();
if ($entity_type === 'file') {
$langcodes = array_keys($attributes['url']['value']);
}
elseif ($langcode = $contenthub_entity
->getAttribute('langcode')) {
$langcodes = $langcode['value'];
}
else {
$langcodes = array_keys($contenthub_entity
->getAttribute('default_langcode')['value'] ?? []);
}
$site_langcodes = array_keys($this->languageManager
->getLanguages());
$supported_languages = array_intersect($site_langcodes, $langcodes);
return !empty($supported_languages);
}
private function importRemoteEntityDependencies(ContentHubEntityDependency $contenthub_entity, array &$dependencies) {
foreach ($contenthub_entity
->getDependencyChain() as $uuid) {
$content_hub_entity_dependency = isset($dependencies[$uuid]) ? $dependencies[$uuid] : FALSE;
if ($content_hub_entity_dependency && !isset($content_hub_entity_dependency->__processed) && $content_hub_entity_dependency
->getRelationship() == ContentHubEntityDependency::RELATIONSHIP_INDEPENDENT) {
$dependencies[$uuid]->__processed = TRUE;
$this
->importRemoteEntityDependencies($content_hub_entity_dependency, $dependencies);
}
}
$trackingEntity = $this->contentHubEntitiesTracking
->loadImportedByUuid($contenthub_entity
->getRawEntity()
->getUuid());
if ($trackingEntity && $trackingEntity
->isAutoUpdateDisabled()) {
return new JsonResponse(NULL);
}
$response = $this
->importRemoteEntityNoDependencies($contenthub_entity);
foreach ($contenthub_entity
->getDependencyChain() as $uuid) {
$content_hub_entity_dependency = isset($dependencies[$uuid]) ? $dependencies[$uuid] : FALSE;
if ($content_hub_entity_dependency && !isset($content_hub_entity_dependency->__processed) && $content_hub_entity_dependency
->getRelationship() == ContentHubEntityDependency::RELATIONSHIP_DEPENDENT) {
$dependencies[$uuid]->__processed = TRUE;
$this
->importRemoteEntityDependencies($content_hub_entity_dependency, $dependencies);
}
}
return $response;
}
private function importRemoteEntityNoDependencies(ContentHubEntityDependency $contenthub_entity) {
$entity_type = $contenthub_entity
->getRawEntity()
->getType();
$class = \Drupal::entityTypeManager()
->getDefinition($entity_type)
->getClass();
$site_origin = $this->contentHubEntitiesTracking
->getSiteOrigin();
if ($contenthub_entity
->getRawEntity()
->getOrigin() == $site_origin) {
$args = [
'@type' => $contenthub_entity
->getRawEntity()
->getType(),
'@uuid' => $contenthub_entity
->getRawEntity()
->getUuid(),
'@origin' => $contenthub_entity
->getRawEntity()
->getOrigin(),
];
$message = $this
->t('Cannot save "@type" entity with uuid="@uuid". It has the same origin as this site: "@origin"', $args);
$this->loggerFactory
->get('acquia_contenthub')
->debug($message);
return $this
->jsonErrorResponseMessage($message, FALSE, 400);
}
try {
$entity = $this->serializer
->deserialize($contenthub_entity
->getRawEntity()
->json(), $class, $this->format);
} catch (\UnexpectedValueException $e) {
$error = $e
->getMessage();
return $this
->jsonErrorResponseMessage($error, FALSE, 400);
}
if (empty($entity)) {
$message = $this
->t('Entity (type = "%type", uuid = "%uuid") cannot be saved.', [
'%type' => $entity_type,
'%uuid' => $contenthub_entity
->getUuid(),
]);
$this->loggerFactory
->get('acquia_contenthub')
->debug($message);
return new JsonResponse(NULL);
}
$transaction = $this->database
->startTransaction();
try {
$entity->__contenthub_entity_syncing = TRUE;
if ($entity instanceof ContentEntityInterface && $entity
->hasField('path') && !$this
->pathAliasMovedToSeparateModule()) {
$languages = $entity
->getTranslationLanguages();
foreach ($languages as $language) {
$entity = $entity
->getTranslation($language
->getId());
$path = $entity
->get('path')
->first()
->getValue();
if (!empty($path['pid']) || !isset($path['alias'])) {
continue;
}
$raw_path = $entity
->id() ? '/' . $entity
->toUrl()
->getInternalPath() : $this
->getPathByAlias($path['alias'], $path['langcode']);
if (!$raw_path) {
continue;
}
$query = \Drupal::database()
->select('url_alias', 'ua')
->fields('ua', [
'pid',
]);
$query
->condition('ua.source', $raw_path);
$query
->condition('ua.langcode', $language
->getId());
$alias = $query
->execute()
->fetchObject();
if (!isset($alias->pid)) {
continue;
}
$path['pid'] = $alias->pid;
$path['source'] = $raw_path;
$entity
->set('path', [
$path,
]);
}
}
if ($entity
->getEntityTypeId() === 'redirect' && $entity
->getHash()) {
$local_redirect_entity = reset(\Drupal::entityTypeManager()
->getStorage('redirect')
->loadByProperties([
'hash' => $entity
->getHash(),
]));
if ($local_redirect_entity && $local_redirect_entity
->uuid() !== $entity
->uuid()) {
$this->loggerFactory
->get('acquia_contenthub')
->debug('An existing redirect entity with the same "hash" was found and overwritten by the one coming from Content Hub. Old UUID = "%old_uuid", New UUID = "%new_uuid", source = "%source", old destination = "%old_destination", new destination = "%new_destination".', [
'%old_uuid' => $local_redirect_entity
->uuid(),
'%new_uuid' => $entity
->uuid(),
'%source' => $local_redirect_entity
->getSourceUrl(),
'%old_destination' => $local_redirect_entity
->getRedirectUrl()
->toUriString(),
'%new_destination' => $entity
->getRedirectUrl()
->toUriString(),
]);
$local_redirect_entity
->delete();
}
}
$entity
->save();
unset($entity->__contenthub_entity_syncing);
$is_new_entity = $this
->trackImportedEntity($contenthub_entity);
if ($is_new_entity && $contenthub_entity
->isEntityDependent()) {
$this
->updateHostEntity($entity);
}
$moduleHandler = \Drupal::service('module_handler');
if ($moduleHandler
->moduleExists('pathauto')) {
$accountSwitcher = \Drupal::service('account_switcher');
$roles = \Drupal::entityTypeManager()
->getStorage('user_role')
->loadByProperties([
'is_admin' => TRUE,
]);
$role = reset($roles);
if ($role) {
$renderUser = new ContentHubUserSession($role
->id());
$accountSwitcher
->switchTo($renderUser);
try {
$alias_storage_helper = \Drupal::service('pathauto.alias_storage_helper');
$languages = $entity
->getTranslationLanguages();
foreach ($languages as $language) {
$entity = $entity
->getTranslation($language
->getId());
if ($entity && $entity
->hasField('path')) {
$path = $entity
->get('path')
->getValue();
if (!$this
->pathAliasMovedToSeparateModule() && ($path = reset($path))) {
$path['source'] = empty($path['source']) ? '/' . $entity
->toUrl()
->getInternalPath() : $path['source'];
$path['language'] = isset($path['langcode']) ? $path['langcode'] : $language
->getId();
$existing_alias = $alias_storage_helper
->loadBySource($path['source'], $language
->getId());
$existing_alias = !empty($existing_alias) ? $existing_alias : NULL;
$alias_storage_helper
->save($path, $existing_alias);
}
}
}
} catch (\Exception $e) {
$this->loggerFactory
->get('acquia_contenthub')
->debug('Could not generate path alias for (%entity_type, %entity_id). Error message: %message', [
'%entity_type' => $entity
->getEntityTypeId(),
'%entity_id' => $entity
->id(),
'%message' => $e
->getMessage(),
]);
}
$accountSwitcher
->switchBack();
}
}
} catch (\Exception $e) {
$transaction
->rollback();
$this->loggerFactory
->get('acquia_contenthub')
->error($e
->getMessage());
throw $e;
}
$serialized_entity = $this->serializer
->normalize($entity, 'json');
return new JsonResponse($serialized_entity);
}
private function updateHostEntity(EntityInterface $entity) {
switch ($entity
->getEntityTypeId()) {
case 'paragraph':
$host_entity = $entity
->getParentEntity();
$field_paragraph = $entity
->get('parent_field_name')
->getString();
$host_entity->{$field_paragraph}
->appendItem($entity);
$host_entity->__contenthub_entity_syncing = TRUE;
$host_entity
->save();
unset($host_entity->__contenthub_entity_syncing);
break;
}
}
private function jsonErrorResponseMessage($message, $status, $status_code = 400) {
$json = [
'status' => $status,
'message' => $message,
];
return new JsonResponse($json, $status_code);
}
private function trackImportedEntity(ContentHubEntityDependency $contenthub_entity) {
$cdf = (array) $contenthub_entity
->getRawEntity();
$imported_entity = $this->contentHubEntitiesTracking
->loadImportedByUuid($cdf['uuid']);
if ($imported_entity) {
if (!$imported_entity
->isDependent()) {
$imported_entity
->setAutoUpdate();
}
$imported_entity
->setModified($cdf['modified']);
$this
->saveImportedEntity();
return FALSE;
}
$entity = $this->entityRepository
->loadEntityByUuid($cdf['type'], $cdf['uuid']);
if ($entity) {
$this->contentHubEntitiesTracking
->setImportedEntity($cdf['type'], $entity
->id(), $cdf['uuid'], $cdf['modified'], $cdf['origin']);
if ($contenthub_entity
->isEntityDependent()) {
$this->contentHubEntitiesTracking
->setDependent();
}
$this
->saveImportedEntity();
return TRUE;
}
$this->loggerFactory
->get('acquia_contenthub')
->error('Error trying to track imported entity with uuid=%uuid, type=%type. Check if the entity exists and is being tracked properly.', [
'%type' => $cdf['type'],
'%uuid' => $cdf['uuid'],
]);
return FALSE;
}
private function saveImportedEntity() {
$success = $this->contentHubEntitiesTracking
->save();
if (!$success) {
$args = [
'%type' => $this->contentHubEntitiesTracking
->getEntityType(),
'%uuid' => $this->contentHubEntitiesTracking
->getUuid(),
];
$message = $this
->t('Imported entity type = %type with uuid=%uuid could not be saved in the tracking table.', $args);
}
else {
$args = [
'%type' => $this->contentHubEntitiesTracking
->getEntityType(),
'%uuid' => $this->contentHubEntitiesTracking
->getUuid(),
];
$message = $this
->t('Saving %type entity with uuid=%uuid. Tracking imported entity with auto updates.', $args);
}
$this->loggerFactory
->get('acquia_contenthub')
->debug($message);
}
public function entityUpdate(EntityInterface $entity) {
if (!empty($entity->__contenthub_entity_syncing)) {
return;
}
$imported_entity = $this->contentHubEntitiesTracking
->loadImportedByDrupalEntity($entity
->getEntityTypeId(), $entity
->id());
if (!$imported_entity || !$imported_entity
->isPendingSync()) {
return;
}
$this
->importRemoteEntity($imported_entity
->getUuid(), $entity);
}
private function findRootAncestorImportEntity(EntityInterface $entity) {
$imported_entity = $this->contentHubEntitiesTracking
->loadImportedByDrupalEntity($entity
->getEntityTypeId(), $entity
->id());
if (!$imported_entity || !$imported_entity
->isDependent()) {
return $imported_entity;
}
return $this
->findRootAncestorImportEntity($this
->getParentEntity($entity));
}
private function getParentEntity(EntityInterface $entity) {
if ($entity
->getEntityTypeId() == 'path_alias') {
$route_params = Url::fromUserInput($entity
->getPath())
->getRouteParameters();
foreach ($route_params as $entity_type_id => $entity_id) {
if (!\Drupal::entityTypeManager()
->hasDefinition($entity_type_id)) {
continue;
}
$entity_from_route = \Drupal::entityTypeManager()
->getStorage($entity_type_id)
->load($entity_id);
if ($entity
->getPath() !== "/{$entity_from_route->toUrl()->getInternalPath()}") {
continue;
}
return $entity_from_route;
}
}
return $entity
->getParentEntity();
}
public function entityDelete(EntityInterface $entity) {
$imported_entity = $this->contentHubEntitiesTracking
->loadImportedByDrupalEntity($entity
->getEntityTypeId(), $entity
->id());
if (!$imported_entity) {
return;
}
$imported_entity
->delete();
}
public function addEntityToImportQueue($uuid, $include_dependencies = TRUE, $author = NULL, $status = 0) {
$item = (object) [
'data' => [],
];
$item->data[] = new ImportQueueItem($uuid, $include_dependencies, $author, $status);
$queue = $this->queue
->get('acquia_contenthub_import_queue');
if ($queue
->createItem($item)) {
return new JsonResponse([
'status' => 200,
'message' => $uuid . ' added to the queue',
], 200);
}
return $this
->jsonErrorResponseMessage('Unable to add ' . $uuid . ' to the import queue', FALSE);
}
private function getPathByAlias($alias, $langcode = NULL) {
$service_id = $this
->pathAliasMovedToSeparateModule() ? 'path_alias.manager' : 'path.alias_manager';
return \Drupal::service($service_id)
->getPathByAlias($alias, $langcode);
}
private function pathAliasMovedToSeparateModule() {
return version_compare(\Drupal::VERSION, '8.8.0', '>=');
}
}