Updated to Drupal 8.6.4, which is PHP 7.3 friendly. Also updated HTMLaw library....
[yaffs-website] / web / core / modules / content_translation / src / Plugin / Validation / Constraint / ContentTranslationSynchronizedFieldsConstraintValidator.php
1 <?php
2
3 namespace Drupal\content_translation\Plugin\Validation\Constraint;
4
5 use Drupal\content_translation\ContentTranslationManagerInterface;
6 use Drupal\content_translation\FieldTranslationSynchronizerInterface;
7 use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
8 use Drupal\Core\Entity\ContentEntityInterface;
9 use Drupal\Core\Entity\EntityTypeManagerInterface;
10 use Drupal\Core\Field\FieldDefinitionInterface;
11 use Symfony\Component\DependencyInjection\ContainerInterface;
12 use Symfony\Component\Validator\Constraint;
13 use Symfony\Component\Validator\ConstraintValidator;
14
15 /**
16  * Checks that synchronized fields are handled correctly in pending revisions.
17  *
18  * As for untranslatable fields, two modes are supported:
19  * - When changes to untranslatable fields are configured to affect all revision
20  *   translations, synchronized field properties can be changed only in default
21  *   revisions.
22  * - When changes to untranslatable fields affect are configured to affect only
23  *   the revision's default translation, synchronized field properties can be
24  *   changed only when editing the default translation. This may lead to
25  *   temporarily desynchronized values, when saving a pending revision for the
26  *   default translation that changes a synchronized property. These are
27  *   actually synchronized when saving changes to the default translation as a
28  *   new default revision.
29  *
30  * @see \Drupal\content_translation\Plugin\Validation\Constraint\ContentTranslationSynchronizedFieldsConstraint
31  * @see \Drupal\Core\Entity\Plugin\Validation\Constraint\EntityUntranslatableFieldsConstraintValidator
32  *
33  * @internal
34  */
35 class ContentTranslationSynchronizedFieldsConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
36
37   /**
38    * The entity type manager.
39    *
40    * @var \Drupal\Core\Entity\EntityTypeManagerInterface
41    */
42   protected $entityTypeManager;
43
44   /**
45    * The content translation manager.
46    *
47    * @var \Drupal\content_translation\ContentTranslationManagerInterface
48    */
49   protected $contentTranslationManager;
50
51   /**
52    * The field translation synchronizer.
53    *
54    * @var \Drupal\content_translation\FieldTranslationSynchronizerInterface
55    */
56   protected $synchronizer;
57
58   /**
59    * ContentTranslationSynchronizedFieldsConstraintValidator constructor.
60    *
61    * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
62    *   The entity type manager.
63    * @param \Drupal\content_translation\ContentTranslationManagerInterface $content_translation_manager
64    *   The content translation manager.
65    * @param \Drupal\content_translation\FieldTranslationSynchronizerInterface $synchronizer
66    *   The field translation synchronizer.
67    */
68   public function __construct(EntityTypeManagerInterface $entity_type_manager, ContentTranslationManagerInterface $content_translation_manager, FieldTranslationSynchronizerInterface $synchronizer) {
69     $this->entityTypeManager = $entity_type_manager;
70     $this->contentTranslationManager = $content_translation_manager;
71     $this->synchronizer = $synchronizer;
72   }
73
74   /**
75    * [@inheritdoc}
76    */
77   public static function create(ContainerInterface $container) {
78     return new static(
79       $container->get('entity_type.manager'),
80       $container->get('content_translation.manager'),
81       $container->get('content_translation.synchronizer')
82     );
83   }
84
85   /**
86    * {@inheritdoc}
87    */
88   public function validate($value, Constraint $constraint) {
89     /** @var \Drupal\content_translation\Plugin\Validation\Constraint\ContentTranslationSynchronizedFieldsConstraint $constraint */
90     /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
91     $entity = $value;
92     if ($entity->isNew() || !$entity->getEntityType()->isRevisionable()) {
93       return;
94     }
95     // When changes to untranslatable fields are configured to affect all
96     // revision translations, we always allow changes in default revisions.
97     if ($entity->isDefaultRevision() && !$entity->isDefaultTranslationAffectedOnly()) {
98       return;
99     }
100     $entity_type_id = $entity->getEntityTypeId();
101     if (!$this->contentTranslationManager->isEnabled($entity_type_id, $entity->bundle())) {
102       return;
103     }
104     $synchronized_properties = $this->getSynchronizedPropertiesByField($entity->getFieldDefinitions());
105     if (!$synchronized_properties) {
106       return;
107     }
108
109     /** @var \Drupal\Core\Entity\ContentEntityInterface $original */
110     $original = $this->getOriginalEntity($entity);
111     $original_translation = $this->getOriginalTranslation($entity, $original);
112     if ($this->hasSynchronizedPropertyChanges($entity, $original_translation, $synchronized_properties)) {
113       if ($entity->isDefaultTranslationAffectedOnly()) {
114         foreach ($entity->getTranslationLanguages(FALSE) as $langcode => $language) {
115           if ($entity->getTranslation($langcode)->hasTranslationChanges()) {
116             $this->context->addViolation($constraint->defaultTranslationMessage);
117             break;
118           }
119         }
120       }
121       else {
122         $this->context->addViolation($constraint->defaultRevisionMessage);
123       }
124     }
125   }
126
127   /**
128    * Checks whether any synchronized property has changes.
129    *
130    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
131    *   The entity being validated.
132    * @param \Drupal\Core\Entity\ContentEntityInterface $original
133    *   The original unchanged entity.
134    * @param string[][] $synchronized_properties
135    *   An associative array of arrays of synchronized field properties keyed by
136    *   field name.
137    *
138    * @return bool
139    *   TRUE if changes in synchronized properties were detected, FALSE
140    *   otherwise.
141    */
142   protected function hasSynchronizedPropertyChanges(ContentEntityInterface $entity, ContentEntityInterface $original, array $synchronized_properties) {
143     foreach ($synchronized_properties as $field_name => $properties) {
144       foreach ($properties as $property) {
145         $items = $entity->get($field_name)->getValue();
146         $original_items = $original->get($field_name)->getValue();
147         if (count($items) !== count($original_items)) {
148           return TRUE;
149         }
150         foreach ($items as $delta => $item) {
151           // @todo This loose comparison is not fully reliable. Revisit this
152           //   after https://www.drupal.org/project/drupal/issues/2941092.
153           if ($items[$delta][$property] != $original_items[$delta][$property]) {
154             return TRUE;
155           }
156         }
157       }
158     }
159     return FALSE;
160   }
161
162   /**
163    * Returns the original unchanged entity to be used to detect changes.
164    *
165    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
166    *   The entity being changed.
167    *
168    * @return \Drupal\Core\Entity\ContentEntityInterface
169    *   The unchanged entity.
170    */
171   protected function getOriginalEntity(ContentEntityInterface $entity) {
172     if (!isset($entity->original)) {
173       $storage = $this->entityTypeManager->getStorage($entity->getEntityTypeId());
174       $original = $entity->isDefaultRevision() ? $storage->loadUnchanged($entity->id()) : $storage->loadRevision($entity->getLoadedRevisionId());
175     }
176     else {
177       $original = $entity->original;
178     }
179     return $original;
180   }
181
182   /**
183    * Returns the original translation.
184    *
185    * @param \Drupal\Core\Entity\ContentEntityInterface $entity
186    *   The entity being validated.
187    * @param \Drupal\Core\Entity\ContentEntityInterface $original
188    *   The original entity.
189    *
190    * @return \Drupal\Core\Entity\ContentEntityInterface
191    *   The original entity translation object.
192    */
193   protected function getOriginalTranslation(ContentEntityInterface $entity, ContentEntityInterface $original) {
194     $langcode = $entity->language()->getId();
195     if ($original->hasTranslation($langcode)) {
196       $original_langcode = $langcode;
197     }
198     else {
199       $metadata = $this->contentTranslationManager->getTranslationMetadata($entity);
200       $original_langcode = $metadata->getSource();
201     }
202     return $original->getTranslation($original_langcode);
203   }
204
205   /**
206    * Returns the synchronized properties for every specified field.
207    *
208    * @param \Drupal\Core\Field\FieldDefinitionInterface[] $field_definitions
209    *   An array of field definitions.
210    *
211    * @return string[][]
212    *   An associative array of arrays of field property names keyed by field
213    *   name.
214    */
215   public function getSynchronizedPropertiesByField(array $field_definitions) {
216     $synchronizer = $this->synchronizer;
217     $synchronized_properties = array_filter(array_map(
218       function (FieldDefinitionInterface $field_definition) use ($synchronizer) {
219         return $synchronizer->getFieldSynchronizedProperties($field_definition);
220       },
221       $field_definitions
222     ));
223     return $synchronized_properties;
224   }
225
226 }