diff --git a/modules/entity_share_client/entity_share_client.routing.yml b/modules/entity_share_client/entity_share_client.routing.yml index 78e6db6..35fb6e6 100644 --- a/modules/entity_share_client/entity_share_client.routing.yml +++ b/modules/entity_share_client/entity_share_client.routing.yml @@ -19,12 +19,11 @@ entity_share_client.admin_content_pull_form: _admin_route: TRUE entity_share_client.diff: - path: '/admin/content/entity_share/pull/diff/{left_revision}/{remote}/{channel_id}/{uuid}' + path: '/admin/content/entity_share/pull/diff/{left_revision_id}/{remote}/{channel_id}/{uuid}' defaults: _controller: '\Drupal\entity_share_client\Controller\DiffController::compareEntities' _title: 'Entity comparison' requirements: _permission: 'entity_share_client_pull_content' - _module_dependencies: 'diff' options: _admin_route: TRUE diff --git a/modules/entity_share_client/entity_share_client.services.yml b/modules/entity_share_client/entity_share_client.services.yml index bd5d504..9aad416 100644 --- a/modules/entity_share_client/entity_share_client.services.yml +++ b/modules/entity_share_client/entity_share_client.services.yml @@ -76,3 +76,17 @@ services: arguments: ['@state'] calls: - [setKeyRepository, ['@?key.repository']] + + plugin.manager.diff_manager: + class: Drupal\entity_share_client\DiffManager + parent: default_plugin_manager + + entity_share_client.entity_parser: + class: Drupal\entity_share_client\Service\EntityParser + arguments: + - '@plugin.manager.diff_manager' + - '@language_manager' + - '@entity_share_client.remote_manager' + - '@entity_share_client.jsonapi_helper' + - '@jsonapi.resource_type.repository' + - '@entity_type.manager' diff --git a/modules/entity_share_client/src/Annotation/DiffGenerator.php b/modules/entity_share_client/src/Annotation/DiffGenerator.php new file mode 100644 index 0000000..ad633f1 --- /dev/null +++ b/modules/entity_share_client/src/Annotation/DiffGenerator.php @@ -0,0 +1,43 @@ +remoteManager = $container->get('entity_share_client.remote_manager'); - $instance->jsonapiHelper = $container->get('entity_share_client.jsonapi_helper'); - $instance->routeMatch = $container->get('current_route_match'); - $instance->dateFormatter = $container->get('date.formatter'); $instance->resourceTypeRepository = $container->get('jsonapi.resource_type.repository'); + $instance->diffFormatter = $container->get('diff.formatter'); + $instance->entityParser = $container->get('entity_share_client.entity_parser'); return $instance; } /** * Returns a table showing the differences between local and remote entities. * - * @param int $left_revision + * @param int $left_revision_id * The revision id of the local entity. * @param \Drupal\entity_share_client\Entity\RemoteInterface $remote * The remote from which the entity is from. @@ -80,7 +68,8 @@ class DiffController extends PluginRevisionController { * @return array * Table showing the diff between the local and remote entities. */ - public function compareEntities($left_revision, RemoteInterface $remote, $channel_id, $uuid) { + public function compareEntities(int $left_revision_id, RemoteInterface $remote, $channel_id, $uuid) { + $build = []; // Reload the remote to have config overrides applied. $remote = $this->entityTypeManager() ->getStorage('remote') @@ -90,7 +79,12 @@ class DiffController extends PluginRevisionController { // Get the left/local revision. $entity_type_id = $channels_infos[$channel_id]['channel_entity_type']; $storage = $this->entityTypeManager()->getStorage($entity_type_id); - $left_revision = $storage->loadRevision($left_revision); + $left_revision = $storage->loadRevision($left_revision_id); + + $this->entityParser->validateNeedToProcess($left_revision->uuid(), FALSE); + $local_values = $this->entityParser->prepareLocalEntity($left_revision); + + $left_yaml = explode("\n", Yaml::encode($local_values)); // Get the right/remote revision. $url = $channels_infos[$channel_id]['url']; @@ -99,84 +93,38 @@ class DiffController extends PluginRevisionController { $response = $this->remoteManager->jsonApiRequest($remote, 'GET', $prepared_url); $json = Json::decode((string) $response->getBody()); - $entity_type = $storage->getEntityType(); - $entity_keys = $entity_type->getKeys(); + // There will be only one result. + $entity_data = current(EntityShareUtility::prepareData($json['data'])); + $this->entityParser->validateNeedToProcess($entity_data['id'], TRUE); + $remote_values = $this->entityParser->prepareRemoteEntity($entity_data, $remote); - $resource_type = $this->resourceTypeRepository->get( - $entity_type_id, - $left_revision->bundle() - ); - $id_public_name = $resource_type->getPublicName($entity_keys['id']); + $right_yaml = explode("\n", Yaml::encode($remote_values)); - // There will be only one result. - foreach (EntityShareUtility::prepareData($json['data']) as $entity_data) { - // Force the remote entity id to be the same as the local entity otherwise - // the diff is not helpful. - $entity_data['attributes'][$id_public_name] = $left_revision->id(); - $right_revision = $this->jsonapiHelper->extractEntity($entity_data); - } - - $build = $this->compareEntityRevisions($this->routeMatch, $left_revision, $right_revision, 'split_fields'); + $build = $this->diffGenerator($left_yaml, $right_yaml); return $build; } /** - * {@inheritdoc} + * Helper. */ - public function compareEntityRevisions(RouteMatchInterface $route_match, ContentEntityInterface $left_revision, ContentEntityInterface $right_revision, $filter) { - $entity = $left_revision; - // Get language from the entity context. - $langcode = $entity->language()->getId(); - - // Get left and right revision in current language. - $left_revision = $left_revision->getTranslation($langcode); - $right_revision = $right_revision->getTranslation($langcode); - - $build = [ - '#title' => $this->t('Changes to %title', ['%title' => $entity->label()]), - 'header' => [ - '#prefix' => '
', - '#suffix' => '
', + public function diffGenerator(array $left_entity, array $right_entity) { + $diff = new Diff($left_entity, $right_entity); + $this->diffFormatter->show_header = FALSE; + $output = $this->diffFormatter->format($diff); + // Add the CSS for the inline diff. + $element['#attached']['library'][] = 'system/diff'; + $element['diff'] = [ + '#type' => 'table', + '#attributes' => [ + 'class' => ['diff'], ], - 'controls' => [ - '#prefix' => '
', - '#suffix' => '
', + '#header' => [ + ['data' => $this->t('Removal'), 'colspan' => '2'], + ['data' => $this->t('Additions'), 'colspan' => '2'], ], + '#rows' => $output, ]; - - // Perform comparison only if both entity revisions loaded successfully. - if ($left_revision != FALSE && $right_revision != FALSE) { - // Build the diff comparison with the plugin. - $plugin = $this->diffLayoutManager->createInstance($filter); - if ($plugin) { - $build = array_merge_recursive($build, $plugin->build($left_revision, $right_revision, $entity)); - unset($build['header']); - unset($build['controls']); - - // Changes diff table header. - $left_changed = ''; - if (method_exists($left_revision, 'getChangedTime')) { - $left_changed = $this->dateFormatter->format($left_revision->getChangedTime(), 'short'); - } - $build['diff']['#header'][0]['data']['#markup'] = $this->t('Local entity: @changed', [ - '@changed' => $left_changed, - ]); - $right_changed = ''; - if (method_exists($right_revision, 'getChangedTime')) { - $right_changed = $this->dateFormatter->format($right_revision->getChangedTime(), 'short'); - } - $build['diff']['#header'][1]['data']['#markup'] = $this->t('Remote entity: @changed', [ - '@changed' => $right_changed, - ]); - - $build['diff']['#prefix'] = '
'; - $build['diff']['#suffix'] = '
'; - $build['diff']['#attributes']['class'][] = 'diff-responsive-table'; - } - } - - $build['#attached']['library'][] = 'diff/diff.general'; - return $build; + return $element; } } diff --git a/modules/entity_share_client/src/DiffGeneratorInterface.php b/modules/entity_share_client/src/DiffGeneratorInterface.php new file mode 100644 index 0000000..efb3ad0 --- /dev/null +++ b/modules/entity_share_client/src/DiffGeneratorInterface.php @@ -0,0 +1,52 @@ + "This is an example string", + * 1 => "Field values or properties", + * ) + * @endcode + * + * @param \Drupal\Core\Field\FieldItemListInterface $field_items + * Represents an entity field. + * + * @return mixed + * An array of strings to be compared. If an empty array is returned it + * means that a field is either empty or no properties need to be compared + * for that field. + * @see \Drupal\entity_share_client\Plugin\diff\CoreFieldDiffParser + */ + public function build(FieldItemListInterface $field_items); + + /** + * Returns if the plugin can be used for the provided field. + * + * @param \Drupal\Core\Field\FieldStorageDefinitionInterface $field_definition + * The field definition that should be checked. + * + * @return bool + * TRUE if the plugin can be used, FALSE otherwise. + */ + public static function isApplicable(FieldStorageDefinitionInterface $field_definition); + +} diff --git a/modules/entity_share_client/src/DiffManager.php b/modules/entity_share_client/src/DiffManager.php new file mode 100644 index 0000000..eb36097 --- /dev/null +++ b/modules/entity_share_client/src/DiffManager.php @@ -0,0 +1,102 @@ +setCacheBackend($cache_backend, 'field_diff_generator_plugins'); + } + + /** + * Creates a plugin instance for a field definition. + * + * Creates the instance based on the selected plugin for the field. + * + * @param string $field_type + * The field type. + * + * @return \Drupal\entity_share_client\DiffGeneratorInterface|null + * The plugin instance, NULL if none. + * + * @throws \Drupal\Component\Plugin\Exception\PluginNotFoundException + * @throws \Drupal\Component\Plugin\Exception\PluginException + */ + public function createInstanceForFieldDefinition(string $field_type) { + if (!isset($this->pluginDefinitions)) { + foreach ($this->getDefinitions() as $plugin_definition) { + if (isset($plugin_definition['field_types'])) { + // Iterate through all the field types this plugin supports + // and for every such field type add the id of the plugin. + if (!isset($plugin_definition['weight'])) { + $plugin_definition['weight'] = 0; + } + + foreach ($plugin_definition['field_types'] as $id) { + $this->pluginDefinitions[$id][$plugin_definition['id']]['weight'] = $plugin_definition['weight']; + } + $plugins = $this->pluginDefinitions; + } + } + } + else { + $plugins = $this->pluginDefinitions; + } + // Build a list of all diff plugins supporting the field type of the field. + $plugin_options = []; + if (isset($plugins[$field_type])) { + // Sort the plugins based on their weight. + uasort($plugins[$field_type], 'Drupal\Component\Utility\SortArray::sortByWeightElement'); + + foreach ($plugins[$field_type] as $id => $weight) { + $definition = $this->getDefinition($id, FALSE); + // Check if the plugin is applicable. + if (isset($definition['class']) && in_array($field_type, $definition['field_types'])) { + $plugin_options[$id] = $this->getDefinitions()[$id]['label']; + } + } + $settings = key($plugin_options); + return $this->createInstance($settings, []); + } + return NULL; + } + +} diff --git a/modules/entity_share_client/src/DiffManagerBase.php b/modules/entity_share_client/src/DiffManagerBase.php new file mode 100644 index 0000000..27a7310 --- /dev/null +++ b/modules/entity_share_client/src/DiffManagerBase.php @@ -0,0 +1,115 @@ +remoteManager = $remoteManager; + $this->entityTypeManager = $entity_type_manager; + $this->entityParser = $entity_parser; + $this->remote = NULL; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_share_client.remote_manager'), + $container->get('entity_type.manager'), + $container->get('entity_share_client.entity_parser') + ); + } + + /** + * Returns Remote entity. + */ + public function getRemote() { + return $this->remote; + } + + /** + * Sets Remote entity. + */ + public function setRemote($remote) { + $this->remote = $remote; + } + + /** + * {@inheritdoc} + */ + public static function isApplicable(FieldStorageDefinitionInterface $field_definition) { + return TRUE; + } + +} diff --git a/modules/entity_share_client/src/Plugin/diff/CommentFieldDiffParser.php b/modules/entity_share_client/src/Plugin/diff/CommentFieldDiffParser.php new file mode 100644 index 0000000..b5f80c8 --- /dev/null +++ b/modules/entity_share_client/src/Plugin/diff/CommentFieldDiffParser.php @@ -0,0 +1,55 @@ + $field_item) { + if (!$field_item->isEmpty()) { + $values = $field_item->getValue(); + + // A more human friendly representation. + if (isset($values['status'])) { + switch ($values['status']) { + case CommentItemInterface::OPEN: + $result[$field_key] = (string) $this->t('Comments for this entity are open.'); + break; + + case CommentItemInterface::CLOSED: + $result[$field_key] = (string) $this->t('Comments for this entity are closed.'); + break; + + case CommentItemInterface::HIDDEN: + $result[$field_key] = (string) $this->t('Comments for this entity are hidden.'); + break; + } + } + } + } + + return $result; + } + +} diff --git a/modules/entity_share_client/src/Plugin/diff/CoreFieldDiffParser.php b/modules/entity_share_client/src/Plugin/diff/CoreFieldDiffParser.php new file mode 100644 index 0000000..376bb4a --- /dev/null +++ b/modules/entity_share_client/src/Plugin/diff/CoreFieldDiffParser.php @@ -0,0 +1,52 @@ +getFieldDefinition(); + $type = $definition->getType(); + + // Every item from $field_items is of type FieldItemInterface. + foreach ($field_items as $field_key => $field_item) { + if (!$field_item->isEmpty()) { + $values = $field_item->getValue(); + if (isset($values['value'])) { + $result[$field_key] = $values['value']; + if ($type == 'boolean') { + $result[$field_key] = ($result[$field_key] == 1); + } + // For some reason local numbers are represented as strings, + // while remote numbers are indeed numbers. In order to avoid fake + // differences, simply cast all numbers to strings. + elseif (in_array($type, ['float', 'integer', 'decimal'])) { + $result[$field_key] = (string) $result[$field_key]; + } + } + } + } + + return $result; + } + +} diff --git a/modules/entity_share_client/src/Plugin/diff/DynamicEntityReferenceFieldDiffParser.php b/modules/entity_share_client/src/Plugin/diff/DynamicEntityReferenceFieldDiffParser.php new file mode 100644 index 0000000..f914a83 --- /dev/null +++ b/modules/entity_share_client/src/Plugin/diff/DynamicEntityReferenceFieldDiffParser.php @@ -0,0 +1,51 @@ +getRemote()) { + foreach ($field_items as $field_key => $field_item) { + if (!$field_item->isEmpty()) { + if ($field_item->entity) { + $entity = $field_item->entity; + $entity_type_id = $entity->getEntityTypeId(); + $result[$field_key] = $entity_type_id . ': ' . $entity->uuid(); + } + } + } + } + + // Case of remote entity. + elseif (!empty($remote_field_data['data'])) { + foreach ($remote_field_data['data'] as $field_key => $remote_item_data) { + list($entity_type_id,) = explode('--', $remote_item_data['type']); + $result[$field_key] = $entity_type_id . ': ' . $remote_item_data['id']; + } + } + + return $result; + } + +} diff --git a/modules/entity_share_client/src/Plugin/diff/EntityReferenceFieldDiffParser.php b/modules/entity_share_client/src/Plugin/diff/EntityReferenceFieldDiffParser.php new file mode 100644 index 0000000..44c95f1 --- /dev/null +++ b/modules/entity_share_client/src/Plugin/diff/EntityReferenceFieldDiffParser.php @@ -0,0 +1,84 @@ +getRemote()) { + foreach ($field_items as $field_key => $field_item) { + if (!$field_item->isEmpty()) { + // Compare entity label. + if ($field_item->entity) { + $entity = $field_item->entity; + // Should we go into recursion and embed the referenced entity? + // If the entity has already been processed, don't embed, + // to avoid infinite loop. + // If the referenced entity type is not Paragraph or Media, + // don't embed. + if ($this->entityParser->referenceEmbeddable($entity->getEntityTypeId()) && + $this->entityParser->validateNeedToProcess($entity->uuid(), FALSE)) { + $result[$field_key] = $this->entityParser->prepareLocalEntity($entity); + } + // If we are not embedding, just show the referenced entity's UUID. + else { + $result[$field_key] = $entity->uuid(); + } + } + } + } + } + + // Case of remote entity. + elseif (!empty($remote_field_data['data'])) { + + $detailed_response = $this->remoteManager->jsonApiRequest($this->getRemote(), 'GET', $remote_field_data['links']['related']['href']); + $entities_json = Json::decode((string) $detailed_response->getBody()); + $data = $entities_json['data'] ?? []; + if (!empty($entities_json['data'])) { + $data = EntityShareUtility::prepareData($entities_json['data']); + } + else { + $data = []; + } + + foreach ($remote_field_data['data'] as $field_key => $remote_item_data) { + $uuid = $data[$field_key]['id']; + list($referenced_entity_type,) = explode('--', $remote_item_data['type']); + if ($this->entityParser->referenceEmbeddable($referenced_entity_type) && + $this->entityParser->validateNeedToProcess($uuid, TRUE)) { + $result[$field_key] = $this->entityParser->prepareRemoteEntity($data[$field_key], $this->getRemote()); + } + else { + $result[$field_key] = $remote_item_data['id']; + } + } + } + + return $result; + } + +} diff --git a/modules/entity_share_client/src/Plugin/diff/EntityReferenceRevisionsFieldDiffParser.php b/modules/entity_share_client/src/Plugin/diff/EntityReferenceRevisionsFieldDiffParser.php new file mode 100644 index 0000000..0faaf2a --- /dev/null +++ b/modules/entity_share_client/src/Plugin/diff/EntityReferenceRevisionsFieldDiffParser.php @@ -0,0 +1,17 @@ +entityTypeManager->getStorage('file'); + + // Every item from $field_items is of type FieldItemInterface. + if (!$this->getRemote()) { + foreach ($field_items as $field_key => $field_item) { + // Even though the local field is empty, remote data may be set. + // So, get field values regardless. + $values = $field_item->getValue(); + + if (!$field_item->isEmpty()) { + // Compare file names. + if (isset($values['target_id'])) { + /** @var \Drupal\file\Entity\File $file */ + $file = $fileManager->load($values['target_id']); + if ($file instanceof File) { + $label = (string) $this->t('File name'); + $result[$field_key][$label] = $file->getFilename(); + $label = (string) $this->t('File size'); + $result[$field_key][$label] = $file->getSize(); + } + } + } + // Compare additional (meta) fields. + foreach ($this->getFieldMetaProperties() as $key => $label) { + if (isset($values[$key])) { + $label = $label; + $result[$field_key][$label] = $values[$key]; + } + } + } + } + elseif (!empty($remote_field_data['data'])) { + + $detailed_response = $this->remoteManager->jsonApiRequest($this->getRemote(), 'GET', $remote_field_data['links']['related']['href']); + $entities_json = Json::decode((string) $detailed_response->getBody()); + $data = $entities_json['data'] ?? []; + if (!empty($entities_json['data'])) { + $data = EntityShareUtility::prepareData($entities_json['data']); + } + else { + $data = []; + } + + foreach ($remote_field_data['data'] as $field_key => $remote_item_data) { + if ($data[$field_key]['attributes']['filename']) { + $label = (string) $this->t('File name'); + $result[$field_key][$label] = $data[$field_key]['attributes']['filename']; + $label = (string) $this->t('File size'); + $result[$field_key][$label] = (string) $data[$field_key]['attributes']['filesize']; + } + // Compare additional (meta) fields. + foreach ($this->getFieldMetaProperties() as $key => $label) { + if (isset($remote_field_data['data'][$field_key]['meta'][$key])) { + $result[$field_key][$label] = $remote_field_data['data'][$field_key]['meta'][$key]; + } + } + } + } + + return $result; + } + + /** + * Declares needed field meta properties. + */ + protected function getFieldMetaProperties() { + return [ + 'description' => (string) $this->t('Description'), + ]; + } + +} diff --git a/modules/entity_share_client/src/Plugin/diff/ImageFieldDiffParser.php b/modules/entity_share_client/src/Plugin/diff/ImageFieldDiffParser.php new file mode 100644 index 0000000..909161e --- /dev/null +++ b/modules/entity_share_client/src/Plugin/diff/ImageFieldDiffParser.php @@ -0,0 +1,28 @@ + (string) $this->t('Alt'), + 'title' => (string) $this->t('Image title'), + ]; + } + +} diff --git a/modules/entity_share_client/src/Plugin/diff/LinkFieldDiffParser.php b/modules/entity_share_client/src/Plugin/diff/LinkFieldDiffParser.php new file mode 100644 index 0000000..1dd9d70 --- /dev/null +++ b/modules/entity_share_client/src/Plugin/diff/LinkFieldDiffParser.php @@ -0,0 +1,47 @@ + $field_item) { + if (!$field_item->isEmpty()) { + $values = $field_item->getValue(); + // Compare the link title. + if (isset($values['title'])) { + $label = (string) $this->t('Title'); + $result[$field_key][$label] = $values['title']; + } + // Compare the uri if exists. + if (isset($values['uri'])) { + $label = (string) $this->t('URL'); + $result[$field_key][$label] = $values['uri']; + } + } + } + + return $result; + } + +} diff --git a/modules/entity_share_client/src/Plugin/diff/ListFieldDiffParser.php b/modules/entity_share_client/src/Plugin/diff/ListFieldDiffParser.php new file mode 100644 index 0000000..cbd5d39 --- /dev/null +++ b/modules/entity_share_client/src/Plugin/diff/ListFieldDiffParser.php @@ -0,0 +1,47 @@ + $field_item) { + // Build the array for comparison only if the field is not empty. + if (!$field_item->isEmpty()) { + $possible_options = $field_item->getPossibleOptions(); + $values = $field_item->getValue(); + if ($possible_options) { + $result[$field_key] = $possible_options[$values['value']] . ' (' . $values['value'] . ')'; + } + else { + $result[$field_key] = $possible_options[$values['value']]; + } + } + } + + return $result; + } + +} diff --git a/modules/entity_share_client/src/Plugin/diff/TextFieldDiffParser.php b/modules/entity_share_client/src/Plugin/diff/TextFieldDiffParser.php new file mode 100644 index 0000000..e2f8744 --- /dev/null +++ b/modules/entity_share_client/src/Plugin/diff/TextFieldDiffParser.php @@ -0,0 +1,41 @@ + $field_item) { + $values = $field_item->getValue(); + // Compare field values. + if (isset($values['value'])) { + // Check if summary or text format are included in the diff. + $label = (string) $this->t('Value'); + $result[$field_key][$label] = $values['value']; + } + } + + return $result; + } + +} diff --git a/modules/entity_share_client/src/Plugin/diff/TextWithSummaryFieldDiffParser.php b/modules/entity_share_client/src/Plugin/diff/TextWithSummaryFieldDiffParser.php new file mode 100644 index 0000000..a413d80 --- /dev/null +++ b/modules/entity_share_client/src/Plugin/diff/TextWithSummaryFieldDiffParser.php @@ -0,0 +1,48 @@ + $field_item) { + $values = $field_item->getValue(); + // Handle the text summary. + if (isset($values['summary'])) { + if ($values['summary'] != "") { + $label = (string) $this->t('Summary'); + $result[$field_key][$label] = $values['summary']; + } + } + + // Compare field values. + if (isset($values['value'])) { + // Check if summary or text format are included in the diff. + $label = (string) $this->t('Value'); + $result[$field_key][$label] = $values['value']; + } + } + + return $result; + } + +} diff --git a/modules/entity_share_client/src/Service/EntityParser.php b/modules/entity_share_client/src/Service/EntityParser.php new file mode 100644 index 0000000..53586ee --- /dev/null +++ b/modules/entity_share_client/src/Service/EntityParser.php @@ -0,0 +1,360 @@ +diffManager = $diff_manager; + $this->languageManager = $language_manager; + $this->remoteManager = $remote_manager; + $this->jsonapiHelper = $jsonapi_helper; + $this->resourceTypeRepository = $resource_type_repository; + $this->entityTypeManager = $entity_type_manager; + $this->entityDefinitions = $entity_type_manager->getDefinitions(); + $this->processedEntities = [ + 'local' => [], + 'remote' => [], + ]; + } + + /** + * {@inheritdoc} + */ + public function prepareLocalEntity(ContentEntityInterface $entity) { + return $this->parseEntity($entity); + } + + /** + * {@inheritdoc} + */ + public function prepareRemoteEntity(array $remote_data, RemoteInterface $remote) { + $remote_entity = $this->jsonapiHelper->extractEntity($remote_data); + return $this->parseEntity($remote_entity, $remote_data, $remote, TRUE); + } + + /** + * {@inheritdoc} + */ + public function validateNeedToProcess(string $uuid, bool $remote) { + $main_key = $remote ? 'remote' : 'local'; + if (!in_array($uuid, $this->processedEntities[$main_key])) { + $this->processedEntities[$main_key][] = $uuid; + return TRUE; + } + return FALSE; + } + + /** + * Parses an entity. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * The Drupal entity (local or remote). + * @param array $remote_data + * Used for remote entity: entity data coming from JSON:API. + * @param \Drupal\entity_share_client\Entity\RemoteInterface $remote + * Used for remote entity: The ES Remote entity. + * @param bool $from_server + * Whether the entity is remote or local. + * + * @return array + * Parsed data of a field, suitable for YAML parsing. + * Associative array, keyed by labels. + * Values are strings, numbers or arrays. + */ + protected function parseEntity(ContentEntityInterface $entity, array $remote_data = NULL, RemoteInterface $remote = NULL, bool $from_server = FALSE) { + $result = []; + $langcode = $this->languageManager->getCurrentLanguage(LanguageInterface::TYPE_CONTENT)->getId(); + if ($from_server) { + $resource_type = $this->getResourceType($entity); + } + // Load entity of current language, otherwise fields are always compared by + // their default language. + if ($entity->hasTranslation($langcode)) { + $entity = $entity->getTranslation($langcode); + } + $irrelevant_fields = $this->getFieldsIrrelevantForDiff($entity); + // Loop through entity fields and transform every FieldItemList object + // into an array of strings according to field type specific settings. + /** @var \Drupal\Core\Field\FieldItemListInterface $field_items */ + foreach ($entity as $item_key => $field_items) { + // Prepare remote information for reference fields, if exists. + $public_key = $from_server ? $resource_type->getPublicName($item_key) : $item_key; + $remote_field_data = []; + // If a field is an entity reference, then eliminate it in + // case the relationship is not handleable. + if ($field_items instanceof EntityReferenceFieldItemListInterface) { + $field_settings = $field_items->getSettings(); + // Determine the referenced entity types by this field. + if (isset($field_settings['target_type'])) { + $referenced_types = [$field_settings['target_type']]; + } + // Dynamic entity reference. + elseif (isset($field_settings['entity_type_ids'])) { + $referenced_types = $field_settings['entity_type_ids']; + } + else { + $referenced_types = []; + } + if ($this->referencedTypesHandlable($referenced_types)) { + $should_parse = TRUE; + if (isset($remote_data['relationships'][$public_key])) { + $remote_field_data = $remote_data['relationships'][$public_key]; + if (isset($remote_field_data['data'])) { + $remote_field_data['data'] = EntityShareUtility::prepareData($remote_field_data['data']); + } + } + } + else { + $should_parse = FALSE; + } + } + else { + $should_parse = !in_array($item_key, $irrelevant_fields); + } + if (!$should_parse) { + continue; + } + $parsed_field = $this->parseField($item_key, $field_items, $remote_field_data, $remote, $from_server); + if ($parsed_field != NULL) { + $field_label = (string) $field_items->getFieldDefinition()->getLabel(); + $result[$field_label] = $parsed_field; + } + } + return $result; + } + + /** + * Parses a field or property of entity. + * + * @param string $item_key + * The field key (machine name). + * @param \Drupal\Core\Field\FieldItemListInterface $field_items + * Field items. + * @param array $remote_field_data + * Used for remote entity: field data coming from JSON:API. + * @param \Drupal\entity_share_client\Entity\RemoteInterface $remote + * Used for remote entity: The ES Remote entity. + * @param bool $from_server + * Whether the entity is remote or local. + * + * @return array + * Parsed data of a field, suitable for YAML parsing. + */ + protected function parseField(string $item_key, FieldItemListInterface $field_items, array $remote_field_data = [], RemoteInterface $remote = NULL, bool $from_server = FALSE) { + $build = []; + $field_type = $field_items->getFieldDefinition()->getType(); + $plugin = $this->diffManager->createInstanceForFieldDefinition($field_type); + if ($plugin) { + if ($from_server) { + $plugin->setRemote($remote); + } + $build = $plugin->build($field_items, $remote_field_data); + if (!empty($build)) { + $cardinality = $field_items->getFieldDefinition()->getFieldStorageDefinition()->getCardinality(); + if ($cardinality == 1 && is_array($build)) { + $build = current($build); + } + } + } + return $build; + } + + /** + * Gets a specific JSON:API resource type based on entity type ID and bundle. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * The Drupal entity (local or remote). + * + * @return \Drupal\jsonapi\ResourceType\ResourceType + * The requested JSON:API resource type, if it exists. NULL otherwise. + */ + public function getResourceType(ContentEntityInterface $entity) { + $resource_type = $this->resourceTypeRepository->get( + $entity->getEntityTypeId(), + $entity->bundle() + ); + return $resource_type; + } + + /** + * Checks if the entity should be embedded into Diff or just listed with UUID. + * + * @param string $entity_type_id + * Entity type ID. + * + * @return bool + * Whether entity of this type is embeddable or not. + */ + public function referenceEmbeddable(string $entity_type_id) { + $embeddable_types = [ + 'paragraph', + 'media', + ]; + return in_array($entity_type_id, $embeddable_types); + } + + /** + * Checks if references to this entity type can be handled or just skipped. + * + * @param string[] $entity_type_ids + * Array of entity type IDs. + * + * @return bool + * Whether references to this entity type can be handled. + */ + public function referencedTypesHandlable(array $entity_type_ids) { + if (empty($entity_type_ids)) { + return FALSE; + } + // All referenced entity types must be supported. + foreach ($entity_type_ids as $entity_type_id) { + if ($entity_type_id == 'user') { + return FALSE; + } + if ($this->entityDefinitions[$entity_type_id]->getGroup() == 'configuration') { + return FALSE; + } + } + return TRUE; + } + + /** + * Helper: lists entity properties/fields which should not appear in Diff. + * + * @param \Drupal\Core\Entity\ContentEntityInterface $entity + * The Drupal entity (local or remote). + * + * @return string[] + * Array of entity properties/fields. + */ + protected function getFieldsIrrelevantForDiff(ContentEntityInterface $entity) { + $field_names = []; + // Entity keys. + $entity_keys = $entity->getEntityType()->getKeys(); + // Label and language code should be displayed in the Diff. + unset($entity_keys['label']); + unset($entity_keys['langcode']); + $field_names = array_values($entity_keys); + // Revision keys. + $revision_keys = array_keys($entity->getEntityType()->getRevisionMetadataKeys()); + $field_names = array_merge($field_names, $revision_keys); + // Other keys. + $other_keys = [ + 'changed', + 'created', + // Related to translation. + 'content_translation_source', + 'content_translation_affected', + 'content_translation_outdated', + 'revision_translation_affected', + // Related to paragraphs. + 'parent_id', + 'parent_type', + 'parent_field_name', + // For some reason getRevisionMetadataKeys() doesn't always return these. + 'revision_timestamp', + 'revision_log', + ]; + $field_names = array_merge($field_names, $other_keys); + return $field_names; + } + +} diff --git a/modules/entity_share_client/src/Service/EntityParserInterface.php b/modules/entity_share_client/src/Service/EntityParserInterface.php new file mode 100644 index 0000000..c92a03b --- /dev/null +++ b/modules/entity_share_client/src/Service/EntityParserInterface.php @@ -0,0 +1,30 @@ +getPublicName($entity_keys['id']); - if ($this->moduleHandler->moduleExists('diff') && + if ( in_array($status_info['info_id'], [ StateInformationInterface::INFO_ID_CHANGED, StateInformationInterface::INFO_ID_NEW_TRANSLATION, @@ -204,7 +204,7 @@ class FormHelper implements FormHelperInterface { $options[$data['id']]['status']['data'] = new FormattableMarkup('@label: @diff_link', [ '@label' => $options[$data['id']]['status']['data'], '@diff_link' => Link::createFromRoute($this->t('Diff'), 'entity_share_client.diff', [ - 'left_revision' => $status_info['local_revision_id'], + 'left_revision_id' => $status_info['local_revision_id'], 'remote' => $remote->id(), 'channel_id' => $channel_id, 'uuid' => $data['id'],