Security update for Core, with self-updated composer
[yaffs-website] / web / core / lib / Drupal / Core / Entity / ContentEntityForm.php
1 <?php
2
3 namespace Drupal\Core\Entity;
4
5 use Drupal\Component\Datetime\TimeInterface;
6 use Drupal\Core\Entity\Display\EntityFormDisplayInterface;
7 use Drupal\Core\Entity\Entity\EntityFormDisplay;
8 use Drupal\Core\Form\FormStateInterface;
9 use Symfony\Component\DependencyInjection\ContainerInterface;
10
11 /**
12  * Entity form variant for content entity types.
13  *
14  * @see \Drupal\Core\ContentEntityBase
15  */
16 class ContentEntityForm extends EntityForm implements ContentEntityFormInterface {
17
18   /**
19    * The entity manager.
20    *
21    * @var \Drupal\Core\Entity\EntityManagerInterface
22    */
23   protected $entityManager;
24
25   /**
26    * The entity being used by this form.
27    *
28    * @var \Drupal\Core\Entity\ContentEntityInterface|\Drupal\Core\Entity\RevisionLogInterface
29    */
30   protected $entity;
31
32   /**
33    * The entity type bundle info service.
34    *
35    * @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
36    */
37   protected $entityTypeBundleInfo;
38
39   /**
40    * The time service.
41    *
42    * @var \Drupal\Component\Datetime\TimeInterface
43    */
44   protected $time;
45
46   /**
47    * Constructs a ContentEntityForm object.
48    *
49    * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
50    *   The entity manager.
51    * @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $entity_type_bundle_info
52    *   The entity type bundle service.
53    * @param \Drupal\Component\Datetime\TimeInterface $time
54    *   The time service.
55    */
56   public function __construct(EntityManagerInterface $entity_manager, EntityTypeBundleInfoInterface $entity_type_bundle_info = NULL, TimeInterface $time = NULL) {
57     $this->entityManager = $entity_manager;
58
59     $this->entityTypeBundleInfo = $entity_type_bundle_info ?: \Drupal::service('entity_type.bundle.info');
60     $this->time = $time ?: \Drupal::service('datetime.time');
61   }
62
63   /**
64    * {@inheritdoc}
65    */
66   public static function create(ContainerInterface $container) {
67     return new static(
68       $container->get('entity.manager'),
69       $container->get('entity_type.bundle.info'),
70       $container->get('datetime.time')
71     );
72   }
73
74   /**
75    * {@inheritdoc}
76    */
77   protected function prepareEntity() {
78     parent::prepareEntity();
79
80     // Hide the current revision log message in UI.
81     if ($this->showRevisionUi() && !$this->entity->isNew() && $this->entity instanceof RevisionLogInterface) {
82       $this->entity->setRevisionLogMessage(NULL);
83     }
84   }
85
86   /**
87    * Returns the bundle entity of the entity, or NULL if there is none.
88    *
89    * @return \Drupal\Core\Entity\EntityInterface|null
90    *   The bundle entity.
91    */
92   protected function getBundleEntity() {
93     if ($bundle_entity_type = $this->entity->getEntityType()->getBundleEntityType()) {
94       return $this->entityTypeManager->getStorage($bundle_entity_type)->load($this->entity->bundle());
95     }
96     return NULL;
97   }
98
99   /**
100    * {@inheritdoc}
101    */
102   public function form(array $form, FormStateInterface $form_state) {
103
104     if ($this->showRevisionUi()) {
105       // Advanced tab must be the first, because other fields rely on that.
106       if (!isset($form['advanced'])) {
107         $form['advanced'] = [
108           '#type' => 'vertical_tabs',
109           '#weight' => 99,
110         ];
111       }
112     }
113
114     $form = parent::form($form, $form_state);
115
116     // Content entity forms do not use the parent's #after_build callback
117     // because they only need to rebuild the entity in the validation and the
118     // submit handler because Field API uses its own #after_build callback for
119     // its widgets.
120     unset($form['#after_build']);
121
122     $this->getFormDisplay($form_state)->buildForm($this->entity, $form, $form_state);
123     // Allow modules to act before and after form language is updated.
124     $form['#entity_builders']['update_form_langcode'] = '::updateFormLangcode';
125
126     if ($this->showRevisionUi()) {
127       $this->addRevisionableFormFields($form);
128     }
129
130     return $form;
131   }
132
133   /**
134    * {@inheritdoc}
135    */
136   public function submitForm(array &$form, FormStateInterface $form_state) {
137     parent::submitForm($form, $form_state);
138     // Update the changed timestamp of the entity.
139     $this->updateChangedTime($this->entity);
140   }
141
142   /**
143    * {@inheritdoc}
144    */
145   public function buildEntity(array $form, FormStateInterface $form_state) {
146     /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
147     $entity = parent::buildEntity($form, $form_state);
148
149     // Mark the entity as requiring validation.
150     $entity->setValidationRequired(!$form_state->getTemporaryValue('entity_validated'));
151
152     // Save as a new revision if requested to do so.
153     if ($this->showRevisionUi() && !$form_state->isValueEmpty('revision')) {
154       $entity->setNewRevision();
155       if ($entity instanceof RevisionLogInterface) {
156         // If a new revision is created, save the current user as
157         // revision author.
158         $entity->setRevisionUserId($this->currentUser()->id());
159         $entity->setRevisionCreationTime($this->time->getRequestTime());
160       }
161     }
162
163     return $entity;
164   }
165
166   /**
167    * {@inheritdoc}
168    *
169    * Button-level validation handlers are highly discouraged for entity forms,
170    * as they will prevent entity validation from running. If the entity is going
171    * to be saved during the form submission, this method should be manually
172    * invoked from the button-level validation handler, otherwise an exception
173    * will be thrown.
174    */
175   public function validateForm(array &$form, FormStateInterface $form_state) {
176     parent::validateForm($form, $form_state);
177     /** @var \Drupal\Core\Entity\ContentEntityInterface $entity */
178     $entity = $this->buildEntity($form, $form_state);
179
180     $violations = $entity->validate();
181
182     // Remove violations of inaccessible fields.
183     $violations->filterByFieldAccess($this->currentUser());
184
185     // In case a field-level submit button is clicked, for example the 'Add
186     // another item' button for multi-value fields or the 'Upload' button for a
187     // File or an Image field, make sure that we only keep violations for that
188     // specific field.
189     $edited_fields = [];
190     if ($limit_validation_errors = $form_state->getLimitValidationErrors()) {
191       foreach ($limit_validation_errors as $section) {
192         $field_name = reset($section);
193         if ($entity->hasField($field_name)) {
194           $edited_fields[] = $field_name;
195         }
196       }
197       $edited_fields = array_unique($edited_fields);
198     }
199     else {
200       $edited_fields = $this->getEditedFieldNames($form_state);
201     }
202
203     // Remove violations for fields that are not edited.
204     $violations->filterByFields(array_diff(array_keys($entity->getFieldDefinitions()), $edited_fields));
205
206     $this->flagViolations($violations, $form, $form_state);
207
208     // The entity was validated.
209     $entity->setValidationRequired(FALSE);
210     $form_state->setTemporaryValue('entity_validated', TRUE);
211
212     return $entity;
213   }
214
215   /**
216    * Gets the names of all fields edited in the form.
217    *
218    * If the entity form customly adds some fields to the form (i.e. without
219    * using the form display), it needs to add its fields here and override
220    * flagViolations() for displaying the violations.
221    *
222    * @param \Drupal\Core\Form\FormStateInterface $form_state
223    *   The current state of the form.
224    *
225    * @return string[]
226    *   An array of field names.
227    */
228   protected function getEditedFieldNames(FormStateInterface $form_state) {
229     return array_keys($this->getFormDisplay($form_state)->getComponents());
230   }
231
232   /**
233    * Flags violations for the current form.
234    *
235    * If the entity form customly adds some fields to the form (i.e. without
236    * using the form display), it needs to add its fields to array returned by
237    * getEditedFieldNames() and overwrite this method in order to show any
238    * violations for those fields; e.g.:
239    * @code
240    * foreach ($violations->getByField('name') as $violation) {
241    *   $form_state->setErrorByName('name', $violation->getMessage());
242    * }
243    * parent::flagViolations($violations, $form, $form_state);
244    * @endcode
245    *
246    * @param \Drupal\Core\Entity\EntityConstraintViolationListInterface $violations
247    *   The violations to flag.
248    * @param array $form
249    *   A nested array of form elements comprising the form.
250    * @param \Drupal\Core\Form\FormStateInterface $form_state
251    *   The current state of the form.
252    */
253   protected function flagViolations(EntityConstraintViolationListInterface $violations, array $form, FormStateInterface $form_state) {
254     // Flag entity level violations.
255     foreach ($violations->getEntityViolations() as $violation) {
256       /** @var \Symfony\Component\Validator\ConstraintViolationInterface $violation */
257       $form_state->setErrorByName(str_replace('.', '][', $violation->getPropertyPath()), $violation->getMessage());
258     }
259     // Let the form display flag violations of its fields.
260     $this->getFormDisplay($form_state)->flagWidgetsErrorsFromViolations($violations, $form, $form_state);
261   }
262
263   /**
264    * Initializes the form state and the entity before the first form build.
265    *
266    * @param \Drupal\Core\Form\FormStateInterface $form_state
267    *   The current state of the form.
268    */
269   protected function init(FormStateInterface $form_state) {
270     // Ensure we act on the translation object corresponding to the current form
271     // language.
272     $this->initFormLangcodes($form_state);
273     $langcode = $this->getFormLangcode($form_state);
274     $this->entity = $this->entity->hasTranslation($langcode) ? $this->entity->getTranslation($langcode) : $this->entity->addTranslation($langcode);
275
276     $form_display = EntityFormDisplay::collectRenderDisplay($this->entity, $this->getOperation());
277     $this->setFormDisplay($form_display, $form_state);
278
279     parent::init($form_state);
280   }
281
282   /**
283    * Initializes form language code values.
284    *
285    * @param \Drupal\Core\Form\FormStateInterface $form_state
286    *   The current state of the form.
287    */
288   protected function initFormLangcodes(FormStateInterface $form_state) {
289     // Store the entity default language to allow checking whether the form is
290     // dealing with the original entity or a translation.
291     if (!$form_state->has('entity_default_langcode')) {
292       $form_state->set('entity_default_langcode', $this->entity->getUntranslated()->language()->getId());
293     }
294     // This value might have been explicitly populated to work with a particular
295     // entity translation. If not we fall back to the most proper language based
296     // on contextual information.
297     if (!$form_state->has('langcode')) {
298       // Imply a 'view' operation to ensure users edit entities in the same
299       // language they are displayed. This allows to keep contextual editing
300       // working also for multilingual entities.
301       $form_state->set('langcode', $this->entityManager->getTranslationFromContext($this->entity)->language()->getId());
302     }
303   }
304
305   /**
306    * {@inheritdoc}
307    */
308   public function getFormLangcode(FormStateInterface $form_state) {
309     $this->initFormLangcodes($form_state);
310     return $form_state->get('langcode');
311   }
312
313   /**
314    * {@inheritdoc}
315    */
316   public function isDefaultFormLangcode(FormStateInterface $form_state) {
317     $this->initFormLangcodes($form_state);
318     return $form_state->get('langcode') == $form_state->get('entity_default_langcode');
319   }
320
321   /**
322    * {@inheritdoc}
323    */
324   protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) {
325     // First, extract values from widgets.
326     $extracted = $this->getFormDisplay($form_state)->extractFormValues($entity, $form, $form_state);
327
328     // Then extract the values of fields that are not rendered through widgets,
329     // by simply copying from top-level form values. This leaves the fields
330     // that are not being edited within this form untouched.
331     foreach ($form_state->getValues() as $name => $values) {
332       if ($entity->hasField($name) && !isset($extracted[$name])) {
333         $entity->set($name, $values);
334       }
335     }
336   }
337
338   /**
339    * {@inheritdoc}
340    */
341   public function getFormDisplay(FormStateInterface $form_state) {
342     return $form_state->get('form_display');
343   }
344
345   /**
346    * {@inheritdoc}
347    */
348   public function setFormDisplay(EntityFormDisplayInterface $form_display, FormStateInterface $form_state) {
349     $form_state->set('form_display', $form_display);
350     return $this;
351   }
352
353   /**
354    * Updates the form language to reflect any change to the entity language.
355    *
356    * There are use cases for modules to act both before and after form language
357    * being updated, thus the update is performed through an entity builder
358    * callback, which allows to support both cases.
359    *
360    * @param string $entity_type_id
361    *   The entity type identifier.
362    * @param \Drupal\Core\Entity\EntityInterface $entity
363    *   The entity updated with the submitted values.
364    * @param array $form
365    *   The complete form array.
366    * @param \Drupal\Core\Form\FormStateInterface $form_state
367    *   The current state of the form.
368    *
369    * @see \Drupal\Core\Entity\ContentEntityForm::form()
370    */
371   public function updateFormLangcode($entity_type_id, EntityInterface $entity, array $form, FormStateInterface $form_state) {
372     $langcode = $entity->language()->getId();
373     $form_state->set('langcode', $langcode);
374
375     // If this is the original entity language, also update the default
376     // langcode.
377     if ($langcode == $entity->getUntranslated()->language()->getId()) {
378       $form_state->set('entity_default_langcode', $langcode);
379     }
380   }
381
382   /**
383    * Updates the changed time of the entity.
384    *
385    * Applies only if the entity implements the EntityChangedInterface.
386    *
387    * @param \Drupal\Core\Entity\EntityInterface $entity
388    *   The entity updated with the submitted values.
389    */
390   public function updateChangedTime(EntityInterface $entity) {
391     if ($entity instanceof EntityChangedInterface) {
392       $entity->setChangedTime($this->time->getRequestTime());
393     }
394   }
395
396   /**
397    * Add revision form fields if the entity enabled the UI.
398    *
399    * @param array $form
400    *   An associative array containing the structure of the form.
401    */
402   protected function addRevisionableFormFields(array &$form) {
403     /** @var ContentEntityTypeInterface $entity_type */
404     $entity_type = $this->entity->getEntityType();
405
406     $new_revision_default = $this->getNewRevisionDefault();
407
408     // Add a log field if the "Create new revision" option is checked, or if the
409     // current user has the ability to check that option.
410     $form['revision_information'] = [
411       '#type' => 'details',
412       '#title' => $this->t('Revision information'),
413       // Open by default when "Create new revision" is checked.
414       '#open' => $new_revision_default,
415       '#group' => 'advanced',
416       '#weight' => 20,
417       '#access' => $new_revision_default || $this->entity->get($entity_type->getKey('revision'))->access('update'),
418       '#optional' => TRUE,
419       '#attributes' => [
420         'class' => ['entity-content-form-revision-information'],
421       ],
422       '#attached' => [
423         'library' => ['core/drupal.entity-form'],
424       ],
425     ];
426
427     $form['revision'] = [
428       '#type' => 'checkbox',
429       '#title' => $this->t('Create new revision'),
430       '#default_value' => $new_revision_default,
431       '#access' => !$this->entity->isNew() && $this->entity->get($entity_type->getKey('revision'))->access('update'),
432       '#group' => 'revision_information',
433     ];
434     // Get log message field's key from definition.
435     $log_message_field = $entity_type->getRevisionMetadataKey('revision_log_message');
436     if ($log_message_field && isset($form[$log_message_field])) {
437       $form[$log_message_field] += [
438         '#group' => 'revision_information',
439         '#states' => [
440           'visible' => [
441             ':input[name="revision"]' => ['checked' => TRUE],
442           ],
443         ],
444       ];
445     }
446   }
447
448   /**
449    * Should new revisions created on default.
450    *
451    * @return bool
452    *   New revision on default.
453    */
454   protected function getNewRevisionDefault() {
455     $new_revision_default = FALSE;
456     $bundle_entity = $this->getBundleEntity();
457     if ($bundle_entity instanceof RevisionableEntityBundleInterface) {
458       // Always use the default revision setting.
459       $new_revision_default = $bundle_entity->shouldCreateNewRevision();
460     }
461     return $new_revision_default;
462   }
463
464   /**
465    * Checks whether the revision form fields should be added to the form.
466    *
467    * @return bool
468    *   TRUE if the form field should be added, FALSE otherwise.
469    */
470   protected function showRevisionUi() {
471     return $this->entity->getEntityType()->showRevisionUi();
472   }
473
474 }