Further Drupal 8.6.4 changes. Some core files were not committed before a commit...
[yaffs-website] / web / core / modules / media / src / Entity / Media.php
1 <?php
2
3 namespace Drupal\media\Entity;
4
5 use Drupal\Core\Entity\EditorialContentEntityBase;
6 use Drupal\Core\Entity\EntityStorageInterface;
7 use Drupal\Core\Entity\EntityTypeInterface;
8 use Drupal\Core\Field\BaseFieldDefinition;
9 use Drupal\Core\StringTranslation\StringTranslationTrait;
10 use Drupal\media\MediaInterface;
11 use Drupal\media\MediaSourceEntityConstraintsInterface;
12 use Drupal\media\MediaSourceFieldConstraintsInterface;
13 use Drupal\user\UserInterface;
14
15 /**
16  * Defines the media entity class.
17  *
18  * @todo Remove default/fallback entity form operation when #2006348 is done.
19  * @see https://www.drupal.org/node/2006348.
20  *
21  * @ContentEntityType(
22  *   id = "media",
23  *   label = @Translation("Media"),
24  *   label_singular = @Translation("media item"),
25  *   label_plural = @Translation("media items"),
26  *   label_count = @PluralTranslation(
27  *     singular = "@count media item",
28  *     plural = "@count media items"
29  *   ),
30  *   bundle_label = @Translation("Media type"),
31  *   handlers = {
32  *     "storage" = "Drupal\media\MediaStorage",
33  *     "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
34  *     "list_builder" = "Drupal\media\MediaListBuilder",
35  *     "access" = "Drupal\media\MediaAccessControlHandler",
36  *     "form" = {
37  *       "default" = "Drupal\media\MediaForm",
38  *       "add" = "Drupal\media\MediaForm",
39  *       "edit" = "Drupal\media\MediaForm",
40  *       "delete" = "Drupal\Core\Entity\ContentEntityDeleteForm",
41  *       "delete-multiple-confirm" = "Drupal\Core\Entity\Form\DeleteMultipleForm",
42  *     },
43  *     "translation" = "Drupal\content_translation\ContentTranslationHandler",
44  *     "views_data" = "Drupal\media\MediaViewsData",
45  *     "route_provider" = {
46  *       "html" = "Drupal\Core\Entity\Routing\AdminHtmlRouteProvider",
47  *     }
48  *   },
49  *   base_table = "media",
50  *   data_table = "media_field_data",
51  *   revision_table = "media_revision",
52  *   revision_data_table = "media_field_revision",
53  *   translatable = TRUE,
54  *   show_revision_ui = TRUE,
55  *   entity_keys = {
56  *     "id" = "mid",
57  *     "revision" = "vid",
58  *     "bundle" = "bundle",
59  *     "label" = "name",
60  *     "langcode" = "langcode",
61  *     "uuid" = "uuid",
62  *     "published" = "status",
63  *   },
64  *   revision_metadata_keys = {
65  *     "revision_user" = "revision_user",
66  *     "revision_created" = "revision_created",
67  *     "revision_log_message" = "revision_log_message",
68  *   },
69  *   bundle_entity_type = "media_type",
70  *   permission_granularity = "bundle",
71  *   admin_permission = "administer media",
72  *   field_ui_base_route = "entity.media_type.edit_form",
73  *   common_reference_target = TRUE,
74  *   links = {
75  *     "add-page" = "/media/add",
76  *     "add-form" = "/media/add/{media_type}",
77  *     "canonical" = "/media/{media}",
78  *     "collection" = "/admin/content/media",
79  *     "delete-form" = "/media/{media}/delete",
80  *     "delete-multiple-form" = "/media/delete",
81  *     "edit-form" = "/media/{media}/edit",
82  *     "revision" = "/media/{media}/revisions/{media_revision}/view",
83  *   }
84  * )
85  */
86 class Media extends EditorialContentEntityBase implements MediaInterface {
87
88   use StringTranslationTrait;
89
90   /**
91    * {@inheritdoc}
92    */
93   public function getName() {
94     $name = $this->getEntityKey('label');
95
96     if (empty($name)) {
97       $media_source = $this->getSource();
98       return $media_source->getMetadata($this, $media_source->getPluginDefinition()['default_name_metadata_attribute']);
99     }
100
101     return $name;
102   }
103
104   /**
105    * {@inheritdoc}
106    */
107   public function label() {
108     return $this->getName();
109   }
110
111   /**
112    * {@inheritdoc}
113    */
114   public function setName($name) {
115     return $this->set('name', $name);
116   }
117
118   /**
119    * {@inheritdoc}
120    */
121   public function getCreatedTime() {
122     return $this->get('created')->value;
123   }
124
125   /**
126    * {@inheritdoc}
127    */
128   public function setCreatedTime($timestamp) {
129     return $this->set('created', $timestamp);
130   }
131
132   /**
133    * {@inheritdoc}
134    */
135   public function getOwner() {
136     return $this->get('uid')->entity;
137   }
138
139   /**
140    * {@inheritdoc}
141    */
142   public function setOwner(UserInterface $account) {
143     return $this->set('uid', $account->id());
144   }
145
146   /**
147    * {@inheritdoc}
148    */
149   public function getOwnerId() {
150     return $this->get('uid')->target_id;
151   }
152
153   /**
154    * {@inheritdoc}
155    */
156   public function setOwnerId($uid) {
157     return $this->set('uid', $uid);
158   }
159
160   /**
161    * {@inheritdoc}
162    */
163   public function getSource() {
164     return $this->bundle->entity->getSource();
165   }
166
167   /**
168    * Update the thumbnail for the media item.
169    *
170    * @param bool $from_queue
171    *   Specifies whether the thumbnail update is triggered from the queue.
172    *
173    * @return \Drupal\media\MediaInterface
174    *   The updated media item.
175    *
176    * @internal
177    *
178    * @todo There has been some disagreement about how to handle updates to
179    *   thumbnails. We need to decide on what the API will be for this.
180    *   https://www.drupal.org/node/2878119
181    */
182   protected function updateThumbnail($from_queue = FALSE) {
183     $this->thumbnail->target_id = $this->loadThumbnail($this->getThumbnailUri($from_queue))->id();
184
185     // Set the thumbnail alt.
186     $media_source = $this->getSource();
187     $plugin_definition = $media_source->getPluginDefinition();
188
189     $this->thumbnail->alt = '';
190     if (!empty($plugin_definition['thumbnail_alt_metadata_attribute'])) {
191       $this->thumbnail->alt = $media_source->getMetadata($this, $plugin_definition['thumbnail_alt_metadata_attribute']);
192     }
193
194     return $this;
195   }
196
197   /**
198    * Loads the file entity for the thumbnail.
199    *
200    * If the file entity does not exist, it will be created.
201    *
202    * @param string $thumbnail_uri
203    *   (optional) The URI of the thumbnail, used to load or create the file
204    *   entity. If omitted, the default thumbnail URI will be used.
205    *
206    * @return \Drupal\file\FileInterface
207    *   The thumbnail file entity.
208    */
209   protected function loadThumbnail($thumbnail_uri = NULL) {
210     $values = [
211       'uri' => $thumbnail_uri ?: $this->getDefaultThumbnailUri(),
212     ];
213
214     $file_storage = $this->entityTypeManager()->getStorage('file');
215
216     $existing = $file_storage->loadByProperties($values);
217     if ($existing) {
218       $file = reset($existing);
219     }
220     else {
221       /** @var \Drupal\file\FileInterface $file */
222       $file = $file_storage->create($values);
223       if ($owner = $this->getOwner()) {
224         $file->setOwner($owner);
225       }
226       $file->setPermanent();
227       $file->save();
228     }
229     return $file;
230   }
231
232   /**
233    * Returns the URI of the default thumbnail.
234    *
235    * @return string
236    *   The default thumbnail URI.
237    */
238   protected function getDefaultThumbnailUri() {
239     $default_thumbnail_filename = $this->getSource()->getPluginDefinition()['default_thumbnail_filename'];
240     return \Drupal::config('media.settings')->get('icon_base_uri') . '/' . $default_thumbnail_filename;
241   }
242
243   /**
244    * Updates the queued thumbnail for the media item.
245    *
246    * @return \Drupal\media\MediaInterface
247    *   The updated media item.
248    *
249    * @internal
250    *
251    * @todo If the need arises in contrib, consider making this a public API,
252    *   by adding an interface that extends MediaInterface.
253    */
254   public function updateQueuedThumbnail() {
255     $this->updateThumbnail(TRUE);
256     return $this;
257   }
258
259   /**
260    * Gets the URI for the thumbnail of a media item.
261    *
262    * If thumbnail fetching is queued, new media items will use the default
263    * thumbnail, and existing media items will use the current thumbnail, until
264    * the queue is processed and the updated thumbnail has been fetched.
265    * Otherwise, the new thumbnail will be fetched immediately.
266    *
267    * @param bool $from_queue
268    *   Specifies whether the thumbnail is being fetched from the queue.
269    *
270    * @return string
271    *   The file URI for the thumbnail of the media item.
272    *
273    * @internal
274    */
275   protected function getThumbnailUri($from_queue) {
276     $thumbnails_queued = $this->bundle->entity->thumbnailDownloadsAreQueued();
277     if ($thumbnails_queued && $this->isNew()) {
278       return $this->getDefaultThumbnailUri();
279     }
280     elseif ($thumbnails_queued && !$from_queue) {
281       return $this->get('thumbnail')->entity->getFileUri();
282     }
283
284     $source = $this->getSource();
285     return $source->getMetadata($this, $source->getPluginDefinition()['thumbnail_uri_metadata_attribute']);
286   }
287
288   /**
289    * Determines if the source field value has changed.
290    *
291    * @return bool
292    *   TRUE if the source field value changed, FALSE otherwise.
293    *
294    * @internal
295    */
296   protected function hasSourceFieldChanged() {
297     $source_field_name = $this->getSource()->getConfiguration()['source_field'];
298     $current_items = $this->get($source_field_name);
299     return isset($this->original) && !$current_items->equals($this->original->get($source_field_name));
300   }
301
302   /**
303    * Determines if the thumbnail should be updated for a media item.
304    *
305    * @param bool $is_new
306    *   Specifies whether the media item is new.
307    *
308    * @return bool
309    *   TRUE if the thumbnail should be updated, FALSE otherwise.
310    */
311   protected function shouldUpdateThumbnail($is_new = FALSE) {
312     // Update thumbnail if we don't have a thumbnail yet or when the source
313     // field value changes.
314     return !$this->get('thumbnail')->entity || $is_new || $this->hasSourceFieldChanged();
315   }
316
317   /**
318    * {@inheritdoc}
319    */
320   public function preSave(EntityStorageInterface $storage) {
321     parent::preSave($storage);
322
323     // If no thumbnail has been explicitly set, use the default thumbnail.
324     if ($this->get('thumbnail')->isEmpty()) {
325       $this->thumbnail->target_id = $this->loadThumbnail()->id();
326     }
327   }
328
329   /**
330    * {@inheritdoc}
331    */
332   public function postSave(EntityStorageInterface $storage, $update = TRUE) {
333     parent::postSave($storage, $update);
334     $is_new = !$update;
335     foreach ($this->translations as $langcode => $data) {
336       if ($this->hasTranslation($langcode)) {
337         $translation = $this->getTranslation($langcode);
338         if ($translation->bundle->entity->thumbnailDownloadsAreQueued() && $translation->shouldUpdateThumbnail($is_new)) {
339           \Drupal::queue('media_entity_thumbnail')->createItem(['id' => $translation->id()]);
340         }
341       }
342     }
343   }
344
345   /**
346    * {@inheritdoc}
347    */
348   public function preSaveRevision(EntityStorageInterface $storage, \stdClass $record) {
349     parent::preSaveRevision($storage, $record);
350
351     $is_new_revision = $this->isNewRevision();
352     if (!$is_new_revision && isset($this->original) && empty($record->revision_log_message)) {
353       // If we are updating an existing media item without adding a
354       // new revision, we need to make sure $entity->revision_log_message is
355       // reset whenever it is empty.
356       // Therefore, this code allows us to avoid clobbering an existing log
357       // entry with an empty one.
358       $record->revision_log_message = $this->original->revision_log_message->value;
359     }
360
361     if ($is_new_revision) {
362       $record->revision_created = self::getRequestTime();
363     }
364   }
365
366   /**
367    * Sets the media entity's field values from the source's metadata.
368    *
369    * Fetching the metadata could be slow (e.g., if requesting it from a remote
370    * API), so this is called by \Drupal\media\MediaStorage::save() prior to it
371    * beginning the database transaction, whereas static::preSave() executes
372    * after the transaction has already started.
373    *
374    * @internal
375    *   Expose this as an API in
376    *   https://www.drupal.org/project/drupal/issues/2992426.
377    */
378   public function prepareSave() {
379     // @todo If the source plugin talks to a remote API (e.g. oEmbed), this code
380     // might be performing a fair number of HTTP requests. This is dangerously
381     // brittle and should probably be handled by a queue, to avoid doing HTTP
382     // operations during entity save. See
383     // https://www.drupal.org/project/drupal/issues/2976875 for more.
384
385     // In order for metadata to be mapped correctly, $this->original must be
386     // set. However, that is only set once parent::save() is called, so work
387     // around that by setting it here.
388     if (!isset($this->original) && $id = $this->id()) {
389       $this->original = $this->entityTypeManager()
390         ->getStorage('media')
391         ->loadUnchanged($id);
392     }
393
394     $media_source = $this->getSource();
395     foreach ($this->translations as $langcode => $data) {
396       if ($this->hasTranslation($langcode)) {
397         $translation = $this->getTranslation($langcode);
398         // Try to set fields provided by the media source and mapped in
399         // media type config.
400         foreach ($translation->bundle->entity->getFieldMap() as $metadata_attribute_name => $entity_field_name) {
401           // Only save value in entity field if empty. Do not overwrite existing
402           // data.
403           if ($translation->hasField($entity_field_name) && ($translation->get($entity_field_name)->isEmpty() || $translation->hasSourceFieldChanged())) {
404             $translation->set($entity_field_name, $media_source->getMetadata($translation, $metadata_attribute_name));
405           }
406         }
407
408         // Try to set a default name for this media item if no name is provided.
409         if ($translation->get('name')->isEmpty()) {
410           $translation->setName($translation->getName());
411         }
412
413         // Set thumbnail.
414         if ($translation->shouldUpdateThumbnail($this->isNew())) {
415           $translation->updateThumbnail();
416         }
417       }
418     }
419   }
420
421   /**
422    * {@inheritdoc}
423    */
424   public function validate() {
425     $media_source = $this->getSource();
426
427     if ($media_source instanceof MediaSourceEntityConstraintsInterface) {
428       $entity_constraints = $media_source->getEntityConstraints();
429       $this->getTypedData()->getDataDefinition()->setConstraints($entity_constraints);
430     }
431
432     if ($media_source instanceof MediaSourceFieldConstraintsInterface) {
433       $source_field_name = $media_source->getConfiguration()['source_field'];
434       $source_field_constraints = $media_source->getSourceFieldConstraints();
435       $this->get($source_field_name)->getDataDefinition()->setConstraints($source_field_constraints);
436     }
437
438     return parent::validate();
439   }
440
441   /**
442    * {@inheritdoc}
443    */
444   public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
445     $fields = parent::baseFieldDefinitions($entity_type);
446
447     $fields['name'] = BaseFieldDefinition::create('string')
448       ->setLabel(t('Name'))
449       ->setRequired(TRUE)
450       ->setTranslatable(TRUE)
451       ->setRevisionable(TRUE)
452       ->setDefaultValue('')
453       ->setSetting('max_length', 255)
454       ->setDisplayOptions('form', [
455         'type' => 'string_textfield',
456         'weight' => -5,
457       ])
458       ->setDisplayConfigurable('form', TRUE)
459       ->setDisplayConfigurable('view', TRUE);
460
461     $fields['thumbnail'] = BaseFieldDefinition::create('image')
462       ->setLabel(t('Thumbnail'))
463       ->setDescription(t('The thumbnail of the media item.'))
464       ->setRevisionable(TRUE)
465       ->setTranslatable(TRUE)
466       ->setDisplayOptions('view', [
467         'type' => 'image',
468         'weight' => 5,
469         'label' => 'hidden',
470         'settings' => [
471           'image_style' => 'thumbnail',
472         ],
473       ])
474       ->setDisplayConfigurable('view', TRUE)
475       ->setReadOnly(TRUE);
476
477     $fields['uid'] = BaseFieldDefinition::create('entity_reference')
478       ->setLabel(t('Authored by'))
479       ->setDescription(t('The user ID of the author.'))
480       ->setRevisionable(TRUE)
481       ->setDefaultValueCallback(static::class . '::getCurrentUserId')
482       ->setSetting('target_type', 'user')
483       ->setTranslatable(TRUE)
484       ->setDisplayOptions('form', [
485         'type' => 'entity_reference_autocomplete',
486         'weight' => 5,
487         'settings' => [
488           'match_operator' => 'CONTAINS',
489           'size' => '60',
490           'autocomplete_type' => 'tags',
491           'placeholder' => '',
492         ],
493       ])
494       ->setDisplayConfigurable('form', TRUE)
495       ->setDisplayOptions('view', [
496         'label' => 'hidden',
497         'type' => 'author',
498         'weight' => 0,
499       ])
500       ->setDisplayConfigurable('view', TRUE);
501
502     $fields['status']
503       ->setDisplayOptions('form', [
504         'type' => 'boolean_checkbox',
505         'settings' => [
506           'display_label' => TRUE,
507         ],
508         'weight' => 100,
509       ])
510       ->setDisplayConfigurable('form', TRUE);
511
512     $fields['created'] = BaseFieldDefinition::create('created')
513       ->setLabel(t('Authored on'))
514       ->setDescription(t('The time the media item was created.'))
515       ->setTranslatable(TRUE)
516       ->setRevisionable(TRUE)
517       ->setDefaultValueCallback(static::class . '::getRequestTime')
518       ->setDisplayOptions('form', [
519         'type' => 'datetime_timestamp',
520         'weight' => 10,
521       ])
522       ->setDisplayConfigurable('form', TRUE)
523       ->setDisplayOptions('view', [
524         'label' => 'hidden',
525         'type' => 'timestamp',
526         'weight' => 0,
527       ])
528       ->setDisplayConfigurable('view', TRUE);
529
530     $fields['changed'] = BaseFieldDefinition::create('changed')
531       ->setLabel(t('Changed'))
532       ->setDescription(t('The time the media item was last edited.'))
533       ->setTranslatable(TRUE)
534       ->setRevisionable(TRUE);
535
536     return $fields;
537   }
538
539   /**
540    * Default value callback for 'uid' base field definition.
541    *
542    * @see ::baseFieldDefinitions()
543    *
544    * @return int[]
545    *   An array of default values.
546    */
547   public static function getCurrentUserId() {
548     return [\Drupal::currentUser()->id()];
549   }
550
551   /**
552    * {@inheritdoc}
553    */
554   public static function getRequestTime() {
555     return \Drupal::time()->getRequestTime();
556   }
557
558 }