5 use Drupal\content_moderation\ModerationInformationInterface;
6 use Drupal\Core\Config\ConfigFactory;
7 use Drupal\Component\Plugin\PluginManagerInterface;
8 use Drupal\Core\Entity\ContentEntityInterface;
9 use Drupal\Component\Diff\Diff;
10 use Drupal\Core\Entity\RevisionLogInterface;
11 use Drupal\Component\Utility\Xss;
14 * Entity comparison service that prepares a diff of a pair of entities.
16 class DiffEntityComparison {
19 * Contains the configuration object factory.
21 * @var \Drupal\Core\Config\ConfigFactoryInterface
23 protected $configFactory;
26 * Wrapper object for simple configuration from diff.plugins.yml.
28 * @var \Drupal\Core\Config\ImmutableConfig
30 protected $pluginsConfig;
35 * @var \Drupal\Core\Diff\DiffFormatter
37 protected $diffFormatter;
40 * A list of all the field types from the system and their definitions.
44 protected $fieldTypeDefinitions;
49 * @var \Drupal\diff\DiffEntityParser
51 protected $entityParser;
54 * The field diff plugin manager service.
56 * @var \Drupal\diff\DiffBuilderManager
58 protected $diffBuilderManager;
61 * The content moderation service, if available.
63 * @var \Drupal\content_moderation\ModerationInformationInterface
65 protected $moderationInformation;
68 * Constructs a DiffEntityComparison object.
70 * @param \Drupal\Core\Config\ConfigFactory $config_factory
71 * The configuration factory.
72 * @param \Drupal\diff\DiffFormatter $diff_formatter
73 * The diff formatter service.
74 * @param \Drupal\Component\Plugin\PluginManagerInterface $plugin_manager
75 * The plugin manager service.
76 * @param \Drupal\diff\DiffEntityParser $entity_parser
78 * @param \Drupal\diff\DiffBuilderManager $diff_builder_manager
79 * The diff builder manager.
81 public function __construct(ConfigFactory $config_factory, DiffFormatter $diff_formatter, PluginManagerInterface $plugin_manager, DiffEntityParser $entity_parser, DiffBuilderManager $diff_builder_manager) {
82 $this->configFactory = $config_factory;
83 $this->pluginsConfig = $this->configFactory->get('diff.plugins');
84 $this->diffFormatter = $diff_formatter;
85 $this->fieldTypeDefinitions = $plugin_manager->getDefinitions();
86 $this->entityParser = $entity_parser;
87 $this->diffBuilderManager = $diff_builder_manager;
91 * This method should return an array of items ready to be compared.
93 * @param \Drupal\Core\Entity\ContentEntityInterface $left_entity
95 * @param \Drupal\Core\Entity\ContentEntityInterface $right_entity
99 * Items ready to be compared by the Diff component.
101 public function compareRevisions(ContentEntityInterface $left_entity, ContentEntityInterface $right_entity) {
104 $left_values = $this->entityParser->parseEntity($left_entity);
105 $right_values = $this->entityParser->parseEntity($right_entity);
107 foreach ($left_values as $left_key => $values) {
108 list (, $field_key) = explode(':', $left_key);
109 // Get the compare settings for this field type.
110 $compare_settings = $this->pluginsConfig->get('fields.' . $field_key);
111 $result[$left_key] = [
112 '#name' => (isset($compare_settings['settings']['show_header']) && $compare_settings['settings']['show_header'] == 0) ? '' : $values['label'],
113 '#settings' => $compare_settings,
117 // Fields which exist on the right entity also.
118 if (isset($right_values[$left_key])) {
119 $result[$left_key]['#data'] += $this->combineFields($left_values[$left_key], $right_values[$left_key]);
120 // Unset the field from the right entity so that we know if the right
121 // entity has any fields that left entity doesn't have.
122 unset($right_values[$left_key]);
124 // This field exists only on the left entity.
126 $result[$left_key]['#data'] += $this->combineFields($left_values[$left_key], []);
130 // Fields which exist only on the right entity.
131 foreach ($right_values as $right_key => $values) {
132 list (, $field_key) = explode(':', $right_key);
133 $compare_settings = $this->pluginsConfig->get('fields.' . $field_key);
134 $result[$right_key] = [
135 '#name' => (isset($compare_settings['settings']['show_header']) && $compare_settings['settings']['show_header'] == 0) ? '' : $values['label'],
136 '#settings' => $compare_settings,
139 $result[$right_key]['#data'] += $this->combineFields([], $right_values[$right_key]);
146 * Combine two fields into an array with keys '#left' and '#right'.
148 * @param array $left_values
149 * Entity field formatted into an array of strings.
150 * @param array $right_values
151 * Entity field formatted into an array of strings.
154 * Array resulted after combining the left and right values.
156 protected function combineFields(array $left_values, array $right_values) {
161 $max = max(array(count($left_values), count($right_values)));
162 for ($delta = 0; $delta < $max; $delta++) {
163 // EXPERIMENTAL: Transform thumbnail from ImageFieldBuilder.
164 // @todo Make thumbnail / rich diff data pluggable.
165 // @see https://www.drupal.org/node/2840566
166 if (isset($left_values[$delta])) {
167 $value = $left_values[$delta];
168 if (isset($value['#thumbnail'])) {
169 $result['#left_thumbnail'][] = $value['#thumbnail'];
172 $result['#left'][] = is_array($value) ? implode("\n", $value) : $value;
175 if (isset($right_values[$delta])) {
176 $value = $right_values[$delta];
177 if (isset($value['#thumbnail'])) {
178 $result['#right_thumbnail'][] = $value['#thumbnail'];
181 $result['#right'][] = is_array($value) ? implode("\n", $value) : $value;
186 // If a field has multiple values combine them into one single string.
187 $result['#left'] = implode("\n", $result['#left']);
188 $result['#right'] = implode("\n", $result['#right']);
194 * Prepare the table rows for #type 'table'.
197 * The source string to compare from.
199 * The target string to compare to.
200 * @param bool $show_header
201 * Display diff context headers. For example, "Line x".
202 * @param array $line_stats
203 * Tracks line numbers across multiple calls to DiffFormatter.
205 * @see \Drupal\Component\Diff\DiffFormatter::format
208 * Array of rows usable with #type => 'table' returned by the core diff
209 * formatter when format a diff.
211 public function getRows($a, $b, $show_header = FALSE, array &$line_stats = NULL) {
212 if (!isset($line_stats)) {
214 'counter' => array('x' => 0, 'y' => 0),
215 'offset' => array('x' => 0, 'y' => 0),
219 // Header is the line counter.
220 $this->diffFormatter->show_header = $show_header;
221 $diff = new Diff($a, $b);
223 return $this->diffFormatter->format($diff);
227 * Splits the strings into lines and counts the resulted number of lines.
232 public function processStateLine(array &$diff) {
233 $data = $diff['#data'];
234 if (isset($data['#left']) && $data['#left'] != '') {
235 if (is_string($data['#left'])) {
236 $diff['#data']['#left'] = explode("\n", $data['#left']);
238 $diff['#data']['#count_left'] = count($diff['#data']['#left']);
241 $diff['#data']['#count_left'] = 0;
242 $diff['#data']['#left'] = [];
244 if (isset($data['#right']) && $data['#right'] != '') {
245 if (is_string($data['#right'])) {
246 $diff['#data']['#right'] = explode("\n", $data['#right']);
248 $diff['#data']['#count_right'] = count($diff['#data']['#right']);
251 $diff['#data']['#count_right'] = 0;
252 $diff['#data']['#right'] = [];
257 * Gets the revision description of the revision.
259 * @param \Drupal\Core\Entity\ContentEntityInterface $revision
260 * The current revision.
261 * @param \Drupal\Core\Entity\ContentEntityInterface $previous_revision
262 * (optional) The previous revision. Defaults to NULL.
265 * The revision log message.
267 public function getRevisionDescription(ContentEntityInterface $revision, ContentEntityInterface $previous_revision = NULL) {
268 $revision_summary = '';
269 // Check if the revision has a revision log message.
270 if ($revision instanceof RevisionLogInterface) {
271 $revision_summary = Xss::filter($revision->getRevisionLogMessage());
274 // @todo Autogenerate summary again.
275 // @see https://www.drupal.org/project/diff/issues/2880936
277 // Add workflow/content moderation state information.
278 if ($state = $this->getModerationState($revision)) {
279 $revision_summary .= " ($state)";
282 return $revision_summary;
286 * Creates an log message based on the changes of entity fields.
288 * @param \Drupal\Core\Entity\ContentEntityInterface $revision
289 * The current revision.
292 * Array of the revision fields with their value and label.
294 protected function summary(ContentEntityInterface $revision) {
296 $entity_type_id = $revision->getEntityTypeId();
297 // Loop through entity fields and transform every FieldItemList object
298 // into an array of strings according to field type specific settings.
299 /** @var \Drupal\Core\Field\FieldItemListInterface $field_items */
300 foreach ($revision as $field_items) {
302 // Create a plugin instance for the field definition.
303 $plugin = $this->diffBuilderManager->createInstanceForFieldDefinition($field_items->getFieldDefinition());
304 if ($plugin && $this->diffBuilderManager->showDiff($field_items->getFieldDefinition()->getFieldStorageDefinition())) {
305 // Create the array with the fields of the entity. Recursive if the
306 // field contains entities.
307 if ($plugin instanceof FieldReferenceInterface) {
308 foreach ($plugin->getEntitiesToDiff($field_items) as $entity_key => $reference_entity) {
309 foreach ($this->summary($reference_entity) as $key => $build) {
310 if ($field_items->getFieldDefinition()->getFieldStorageDefinition()->getCardinality() != 1) {
313 $result[$key] = $build;
314 $delta = $show_delta ? '<sub>' . ($entity_key + 1) . '</sub> ' : ' - ';
315 $result[$key]['label'] = $field_items->getFieldDefinition()->getLabel() . $delta . $result[$key]['label'];
320 // Create a unique flat key.
321 $key = $revision->id() . ':' . $entity_type_id . '.' . $field_items->getName();
323 $result[$key]['value'] = $field_items->getValue();
324 $result[$key]['label'] = $field_items->getFieldDefinition()->getLabel();
333 * Gets the revision's content moderation state, if available.
335 * @param \Drupal\Core\Entity\ContentEntityInterface $entity
336 * The entity revision.
338 * @return string|bool
339 * Returns the label of the moderation state, if available, otherwise FALSE.
341 protected function getModerationState(ContentEntityInterface $entity) {
342 if ($this->moderationInformation && $this->moderationInformation->isModeratedEntity($entity)) {
343 if ($state = $entity->moderation_state->value) {
344 $workflow = $this->moderationInformation->getWorkflowForEntity($entity);
345 return $workflow->getTypePlugin()->getState($state)->label();
353 * Sets the content moderation service if available.
355 * @param \Drupal\content_moderation\ModerationInformationInterface $moderation_information
356 * The moderation information service.
358 public function setModerationInformation(ModerationInformationInterface $moderation_information) {
359 $this->moderationInformation = $moderation_information;