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