diff --git a/2831274_61.patch b/2831274_61.patch new file mode 100644 index 0000000000000000000000000000000000000000..16ffcbdb8268d337a88db656ac7367457b6ff1ad GIT binary patch literal 173557 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񦏋"$>H)iY(DxvGg-"d7gt_a%@@@@@@@@@@@@@@@@@@@@@3Ѥ;%,rJrS 7==-ubB!LܔEAZ???i;gqJ%Bv}nn΂f|dttTlQp:V5Mz-ԗrIqǣB)ioo u#!̂f1111<<ޕx  E9ONN~uuu"XX]qᇆǍBL(d2L+uc% qŸe}[[5wds)bwoYww(^v;r4e?nP0%S3sM*GP<55Յ MMMKKK9ԏ +pp*ahDĴbfqݲ +VѺB +{ 9dy׸׮ԽiTI4 G!/uGrXKX*J^C:QX*XHqn(et rR9jntUΕRF"Uk"reJm---Rş8yiHˋQS+~xd!n,lpe3   H뉨#l6i&5 %CJ<y?>>nnn!kr4$HDѮ.3 95H6tH)~ E9jMR5吣H>rrrrrșO@9Ltu8^X7rK\A#%L+_ V j|dz9~&NgYHR\NC9ޞDoiJ&ڒ+5쫫tl|!lu=a$Y>xwvv<|M0>H@@@@@@@@@@@@@@@@@@@@@5O +a?IENDB` \ No newline at end of file diff --git a/core/modules/media_entity/js/media_bundle_form.js b/core/modules/media_entity/js/media_bundle_form.js new file mode 100644 index 0000000000000000000000000000000000000000..7db10c55344183b262a23fc67a540c7add16a948 --- /dev/null +++ b/core/modules/media_entity/js/media_bundle_form.js @@ -0,0 +1,45 @@ +/** + * @file + * Javascript for the media bundle form. + */ + +(function ($, Drupal) { + 'use strict'; + + /** + * Behaviors for setting summaries on media bundle form. + * + * @type {Drupal~behavior} + * + * @prop {Drupal~behaviorAttach} attach + * Attaches summary behaviors on media bundle edit forms. + */ + Drupal.behaviors.mediaBundles = { + attach: function (context) { + var $context = $(context); + // Provide the vertical tab summaries. + $context.find('#edit-workflow').drupalSetSummary(function (context) { + var vals = []; + $(context).find('input[name^="options"]:checked').parent().each(function () { + vals.push(Drupal.checkPlain($(this).find('label').text())); + }); + if (!$(context).find('#edit-options-status').is(':checked')) { + vals.unshift(Drupal.t('Not published')); + } + return vals.join(', '); + }); + $(context).find('#edit-language').drupalSetSummary(function (context) { + var vals = []; + + vals.push($(context).find('.js-form-item-language-configuration-langcode select option:selected').text()); + + $(context).find('input:checked').next('label').each(function () { + vals.push(Drupal.checkPlain($(this).text())); + }); + + return vals.join(', '); + }); + } + }; + +})(jQuery, Drupal); diff --git a/core/modules/media_entity/js/media_form.js b/core/modules/media_entity/js/media_form.js new file mode 100644 index 0000000000000000000000000000000000000000..e8c95476b71d95136d6b9642b3878ec5b3743f80 --- /dev/null +++ b/core/modules/media_entity/js/media_form.js @@ -0,0 +1,55 @@ +/** + * @file + * Defines Javascript behaviors for the media entity form. + */ + +(function ($, Drupal, drupalSettings) { + + 'use strict'; + + /** + * Behaviors for tabs in the media edit form. + * + * @type {Drupal~behavior} + * + * @prop {Drupal~behaviorAttach} attach + * Attaches summary behavior for tabs in the media edit form. + */ + Drupal.behaviors.mediaDetailsSummaries = { + attach: function (context) { + var $context = $(context); + $context.find('.media-form-revision-information').drupalSetSummary(function (context) { + var $revisionContext = $(context); + var revisionCheckbox = $revisionContext.find('.js-form-item-revision input'); + + // Return 'New revision' if the 'Create new revision' checkbox is + // checked, or if the checkbox doesn't exist, but the revision log does. + // For users without the "Administer content" permission the checkbox + // won't appear, but the revision log will if the content type is set to + // auto-revision. + if (revisionCheckbox.is(':checked') || (!revisionCheckbox.length && $revisionContext.find('.js-form-item-revision-log textarea').length)) { + return Drupal.t('New revision'); + } + + return Drupal.t('No revision'); + }); + + $context.find('.media-form-author').drupalSetSummary(function (context) { + var $authorContext = $(context); + var name = $authorContext.find('.field--name-uid input').val(); + var date = $authorContext.find('.field--name-created input').val(); + + if (name && date) { + return Drupal.t('By @name on @date', {'@name': name, '@date': date}); + } + else if (name) { + return Drupal.t('By @name', {'@name': name}); + } + else if (date) { + return Drupal.t('Authored on @date', {'@date': date}); + } + }); + } + }; + +})(jQuery, Drupal, drupalSettings); diff --git a/core/modules/media_entity/media_entity.api.php b/core/modules/media_entity/media_entity.api.php new file mode 100644 index 0000000000000000000000000000000000000000..040d47cef294b85829494376845a865957db3440 --- /dev/null +++ b/core/modules/media_entity/media_entity.api.php @@ -0,0 +1,25 @@ +get('icon_base'); + media_entity_copy_icons($source, $destination); +} + +/** + * Implements hook_update_last_removed(). + * + * Media entity module lived in contrib before being adopted by core. 8003 was + * last schema version before that happened and this should make sure existing + * sites update to last contrib before using core version of the module. + */ +function media_entity_update_last_removed() { + return 8003; +} diff --git a/core/modules/media_entity/media_entity.libraries.yml b/core/modules/media_entity/media_entity.libraries.yml new file mode 100644 index 0000000000000000000000000000000000000000..cd280bc8d55fb08316492fd727881f49a23f88a8 --- /dev/null +++ b/core/modules/media_entity/media_entity.libraries.yml @@ -0,0 +1,13 @@ +media_form: + version: VERSION + js: + 'js/media_form.js': {} + dependencies: + - core/drupal.form + +media_bundle_form: + version: VERSION + js: + 'js/media_bundle_form.js': {} + dependencies: + - core/drupal.form diff --git a/core/modules/media_entity/media_entity.links.action.yml b/core/modules/media_entity/media_entity.links.action.yml new file mode 100644 index 0000000000000000000000000000000000000000..cb5492ca960ce9b3a6daa49ad7d5721865d8a2ac --- /dev/null +++ b/core/modules/media_entity/media_entity.links.action.yml @@ -0,0 +1,11 @@ +media.bundle_add: + route_name: entity.media_bundle.add_form + title: 'Add media bundle' + appears_on: + - entity.media_bundle.collection + +media.add: + route_name: entity.media.add_page + title: 'Add media' + weight: 10 + diff --git a/core/modules/media_entity/media_entity.links.contextual.yml b/core/modules/media_entity/media_entity.links.contextual.yml new file mode 100644 index 0000000000000000000000000000000000000000..cce5f83d5f91ed1d14642cea01d9bfb1ef0a0680 --- /dev/null +++ b/core/modules/media_entity/media_entity.links.contextual.yml @@ -0,0 +1,9 @@ +entity.media.edit_form: + route_name: entity.media.edit_form + group: media + title: Edit +entity.media.delete_form: + route_name: entity.media.delete_form + group: media + title: Delete + weight: 10 diff --git a/core/modules/media_entity/media_entity.links.menu.yml b/core/modules/media_entity/media_entity.links.menu.yml new file mode 100644 index 0000000000000000000000000000000000000000..713c668b908a3f71f62f75d9e5bedd230ae11796 --- /dev/null +++ b/core/modules/media_entity/media_entity.links.menu.yml @@ -0,0 +1,11 @@ +entity.media_bundle.collection: + title: 'Media bundles' + parent: system.admin_structure + description: 'Manage media bundles.' + route_name: entity.media_bundle.collection + +entity.media.add: + title: 'Add a new media' + parent: entity.media.collection + description: 'Add a new media entity.' + route_name: entity.media.add_page diff --git a/core/modules/media_entity/media_entity.links.task.yml b/core/modules/media_entity/media_entity.links.task.yml new file mode 100644 index 0000000000000000000000000000000000000000..c04484799390a0faea667d14ff2b844bc3f6fee2 --- /dev/null +++ b/core/modules/media_entity/media_entity.links.task.yml @@ -0,0 +1,21 @@ +entity.media.canonical: + route_name: entity.media.canonical + base_route: entity.media.canonical + title: 'View' +entity.media.edit_form: + route_name: entity.media.edit_form + base_route: entity.media.canonical + title: Edit +entity.media.delete_form: + route_name: entity.media.delete_form + base_route: entity.media.canonical + title: Delete + weight: 10 +entity.media_bundle.edit_form: + title: 'Edit' + route_name: entity.media_bundle.edit_form + base_route: entity.media_bundle.edit_form +entity.media_bundle.collection: + title: List + route_name: entity.media_bundle.collection + base_route: entity.media_bundle.collection diff --git a/core/modules/media_entity/media_entity.module b/core/modules/media_entity/media_entity.module new file mode 100644 index 0000000000000000000000000000000000000000..56c649333e1375248e32a745ee8e22c95ca0de78 --- /dev/null +++ b/core/modules/media_entity/media_entity.module @@ -0,0 +1,83 @@ +' . t('About') . ''; + $output .= '

' . t('The Media entity module provides a "base" entity for media. This is a very basic entity which can reference to all kinds of media-objects (local files, YouTube videos, Tweets, Instagram photos, ...). Media entity provides a relation between your website and the media resource. You can reference to/use this entity within any other entity on your site. For more information, see the online documentation for the Media entity module.', + [ + ':media_entity_url' => 'https://www.drupal.org/project/media_entity', + ':media_entity_handbook' => 'https://drupal-media.gitbooks.io/drupal8-guide/content/modules/media_entity/intro.html', + ]) . '

'; + $output .= '

' . t('Uses') . '

'; + $output .= '

' . t('For detailed information about the usage of this module please refer to the official documentation.', + [ + ':media_entity_handbook' => 'https://drupal-media.gitbooks.io/drupal8-guide/content/modules/media_entity/intro.html', + ]) . '

'; + + return $output; + } +} + +/** + * Implements hook_theme(). + */ +function media_entity_theme() { + return [ + 'media' => [ + 'render element' => 'elements', + 'file' => 'media_entity.theme.inc', + 'template' => 'media', + ], + ]; +} + +/** + * Implements hook_theme_suggestions_HOOK(). + */ +function media_entity_theme_suggestions_media(array $variables) { + $suggestions = []; + $media = $variables['elements']['#media']; + $sanitized_view_mode = strtr($variables['elements']['#view_mode'], '.', '_'); + + $suggestions[] = 'media__' . $sanitized_view_mode; + $suggestions[] = 'media__' . $media->bundle(); + $suggestions[] = 'media__' . $media->bundle() . '__' . $sanitized_view_mode; + + return $suggestions; +} + +/** + * Copy the media file icons to files directory for use with image styles. + * + * @param string $source + * Source folder. + * @param string $destination + * Destination folder. + * + * @throws Exception + * Thrown when media icons can't be copied to their destination. + */ +function media_entity_copy_icons($source, $destination) { + if (!file_prepare_directory($destination, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) { + throw new Exception("Unable to create directory $destination."); + } + + $files = file_scan_directory($source, '/.*\.(png|jpg)$/'); + foreach ($files as $file) { + $result = file_unmanaged_copy($file->uri, $destination, FILE_EXISTS_REPLACE); + if (!$result) { + throw new Exception("Unable to copy {$file->uri} to $destination."); + } + } +} diff --git a/core/modules/media_entity/media_entity.permissions.yml b/core/modules/media_entity/media_entity.permissions.yml new file mode 100644 index 0000000000000000000000000000000000000000..072259e1d4e22611d0cc8155d5f01fe2b119062d --- /dev/null +++ b/core/modules/media_entity/media_entity.permissions.yml @@ -0,0 +1,18 @@ +administer media: + title: 'Administer media' + restrict access: TRUE +administer media bundles: + title: 'Administer media bundles' + restrict access: TRUE +view media: + title: 'View media' +update media: + title: 'Update own media' +update any media: + title: 'Update any media' +delete media: + title: 'Delete own media' +delete any media: + title: 'Delete any media' +create media: + title: 'Create media' diff --git a/core/modules/media_entity/media_entity.routing.yml b/core/modules/media_entity/media_entity.routing.yml new file mode 100644 index 0000000000000000000000000000000000000000..47771860cea57626da4117ace5eb6cceb0db40f8 --- /dev/null +++ b/core/modules/media_entity/media_entity.routing.yml @@ -0,0 +1,6 @@ +entity.media.multiple_delete_confirm: + path: '/admin/content/media/delete' + defaults: + _form: '\Drupal\media_entity\Form\DeleteMultiple' + requirements: + _permission: 'delete any media' diff --git a/core/modules/media_entity/media_entity.services.yml b/core/modules/media_entity/media_entity.services.yml new file mode 100644 index 0000000000000000000000000000000000000000..a227b7378ccbe38ca7f3d4fe432cb1a300985862 --- /dev/null +++ b/core/modules/media_entity/media_entity.services.yml @@ -0,0 +1,4 @@ +services: + plugin.manager.media_entity.type: + class: Drupal\media_entity\MediaTypeManager + parent: default_plugin_manager diff --git a/core/modules/media_entity/media_entity.theme.inc b/core/modules/media_entity/media_entity.theme.inc new file mode 100644 index 0000000000000000000000000000000000000000..1efeee6aaccf57154ecf3b3d2ead8fb5b7b8476a --- /dev/null +++ b/core/modules/media_entity/media_entity.theme.inc @@ -0,0 +1,38 @@ +label(); + + // Helpful $content variable for templates. + foreach (Element::children($variables['elements']) as $key) { + $variables['content'][$key] = $variables['elements'][$key]; + } + + $variables['attributes']['class'][] = 'media'; + $variables['attributes']['class'][] = Html::getClass('media-' . $variables['media']->bundle()); + $variables['attributes']['class'][] = Html::getClass('view-mode-' . $variables['elements']['#view_mode']); + if (!$variables['media']->isPublished()) { + $variables['attributes']['class'][] = 'unpublished'; + } +} diff --git a/core/modules/media_entity/media_entity.tokens.inc b/core/modules/media_entity/media_entity.tokens.inc new file mode 100644 index 0000000000000000000000000000000000000000..00559254ac7a408c41f88b942e81fe212bb29581 --- /dev/null +++ b/core/modules/media_entity/media_entity.tokens.inc @@ -0,0 +1,180 @@ + t('Media'), + 'description' => t('Tokens related to individual media items.'), + 'needs-data' => 'media', + ]; + + // Core tokens for media. + $media['mid'] = [ + 'name' => t('Media ID'), + 'description' => t('The unique ID of the media item.'), + ]; + $media['uuid'] = [ + 'name' => t('Media UUID'), + 'description' => t('The unique UUID of the media item.'), + ]; + $media['vid'] = [ + 'name' => t('Revision ID'), + 'description' => t("'The unique ID of the media's latest revision."), + ]; + $media['bundle'] = [ + 'name' => t('Media bundle'), + ]; + $media['bundle-name'] = [ + 'name' => t('Media bundle name'), + 'description' => t('The human-readable name of the media bundle.'), + ]; + $media['langcode'] = [ + 'name' => t('Language code'), + 'description' => t('The language code of the language the media is written in.'), + ]; + $media['name'] = [ + 'name' => t('Name'), + 'description' => t('The name of this media.'), + ]; + $node['author'] = [ + 'name' => t('Author'), + 'type' => 'user', + ]; + $media['url'] = [ + 'name' => t('URL'), + 'description' => t('The URL of the media.'), + ]; + $media['edit-url'] = [ + 'name' => t('Edit URL'), + 'description' => t("The URL of the media's edit page."), + ]; + + // Chained tokens for media. + $media['created'] = [ + 'name' => t('Date created'), + 'type' => 'date', + ]; + $media['changed'] = [ + 'name' => t('Date changed'), + 'description' => t('The date the media was most recently updated.'), + 'type' => 'date', + ]; + + return [ + 'types' => ['media' => $type], + 'tokens' => ['media' => $media], + ]; +} + +/** + * Implements hook_tokens(). + */ +function media_entity_tokens($type, $tokens, array $data, array $options, BubbleableMetadata $bubbleable_metadata) { + $token_service = \Drupal::token(); + + $url_options = ['absolute' => TRUE]; + if (isset($options['langcode'])) { + $url_options['language'] = \Drupal::languageManager()->getLanguage($options['langcode']); + $langcode = $options['langcode']; + } + else { + $langcode = LanguageInterface::LANGCODE_DEFAULT; + } + + $replacements = []; + if ($type == 'media' && !empty($data['media'])) { + /** @var \Drupal\media_entity\MediaInterface $media */ + $media = \Drupal::service('entity.repository')->getTranslationFromContext($data['media'], $langcode, ['operation' => 'media_entity_tokens']); + + foreach ($tokens as $name => $original) { + switch ($name) { + // Simple key values on the media_entity. + case 'mid': + $replacements[$original] = $media->id(); + break; + + case 'uuid': + $replacements[$original] = $media->uuid(); + break; + + case 'vid': + $replacements[$original] = $media->getRevisionId(); + break; + + case 'bundle': + $replacements[$original] = $media->bundle(); + break; + + case 'bundle-name': + $replacements[$original] = $media->bundle->entity->label(); + break; + + case 'langcode': + $replacements[$original] = $media->language()->getId(); + break; + + case 'name': + $replacements[$original] = $media->name->value; + break; + + case 'url': + $replacements[$original] = $media->toUrl('canonical', $url_options); + break; + + case 'edit-url': + $replacements[$original] = $media->toUrl('edit-form', $url_options); + break; + + // Default values for the chained tokens handled below. + case 'author': + /** @var \Drupal\user\UserInterface $account */ + $account = $media->getOwner(); + $bubbleable_metadata->addCacheableDependency($account); + $replacements[$original] = $account->label(); + break; + + case 'created': + $date_format = DateFormat::load('medium'); + $bubbleable_metadata->addCacheableDependency($date_format); + $replacements[$original] = \Drupal::service('date.formatter') + ->format($media->getCreatedTime(), 'medium', '', NULL, $langcode); + break; + + case 'changed': + $date_format = DateFormat::load('medium'); + $bubbleable_metadata->addCacheableDependency($date_format); + $replacements[$original] = \Drupal::service('date.formatter') + ->format($media->getChangedTime(), 'medium', '', NULL, $langcode); + break; + } + } + + if ($author_tokens = $token_service->findWithPrefix($tokens, 'author')) { + $account = $media->get('uid')->entity; + $replacements += $token_service->generate('user', $author_tokens, ['user' => $account], $options, $bubbleable_metadata); + } + + if ($created_tokens = $token_service->findWithPrefix($tokens, 'created')) { + $replacements += $token_service->generate('date', $created_tokens, ['date' => $media->getCreatedTime()], $options, $bubbleable_metadata); + } + + if ($changed_tokens = $token_service->findWithPrefix($tokens, 'changed')) { + $replacements += $token_service->generate('date', $changed_tokens, ['date' => $media->getChangedTime()], $options, $bubbleable_metadata); + } + } + + return $replacements; +} diff --git a/core/modules/media_entity/src/Annotation/MediaType.php b/core/modules/media_entity/src/Annotation/MediaType.php new file mode 100644 index 0000000000000000000000000000000000000000..3566c1f7aa9c674534445e31f94f0bb4073379ca --- /dev/null +++ b/core/modules/media_entity/src/Annotation/MediaType.php @@ -0,0 +1,43 @@ +get('created')->value; + } + + /** + * {@inheritdoc} + */ + public function setCreatedTime($timestamp) { + $this->set('created', $timestamp); + return $this; + } + + /** + * {@inheritdoc} + */ + public function getOwner() { + return $this->get('uid')->entity; + } + + /** + * {@inheritdoc} + */ + public function setOwner(UserInterface $account) { + $this->set('uid', $account->id()); + return $this; + } + + /** + * {@inheritdoc} + */ + public function getOwnerId() { + return $this->get('uid')->target_id; + } + + /** + * {@inheritdoc} + */ + public function setOwnerId($uid) { + $this->set('uid', $uid); + return $this; + } + + /** + * {@inheritdoc} + */ + public function getPublisher() { + return $this->getOwner(); + } + + /** + * {@inheritdoc} + */ + public function getPublisherId() { + return $this->getOwnerId(); + } + + /** + * {@inheritdoc} + */ + public function setPublisherId($uid) { + return $this->setOwnerId($uid); + } + + /** + * {@inheritdoc} + */ + public function getType() { + return $this->bundle->entity->getType(); + } + + /** + * {@inheritdoc} + */ + public function preSave(EntityStorageInterface $storage) { + parent::preSave($storage); + + // Try to set fields provided by type plugin and mapped in bundle + // configuration. + foreach ($this->bundle->entity->getFieldMap() as $source_field => $destination_field) { + // Only save value in entity field if empty. Do not overwrite existing + // data. + if ($this->hasField($destination_field) && $this->{$destination_field}->isEmpty() && ($value = $this->getType()->getField($this, $source_field))) { + $this->set($destination_field, $value); + } + } + + // Try to set a default name for this media if no label is provided. + if (!$this->label()) { + $this->set('name', $this->getType()->getDefaultName($this)); + } + + // Set thumbnail. + if (!$this->get('thumbnail')->entity) { + $this->automaticallySetThumbnail(); + } + + } + + /** + * {@inheritdoc} + */ + public function postSave(EntityStorageInterface $storage, $update = TRUE) { + parent::postSave($storage, $update); + if (!$update && $this->bundle->entity->getQueueThumbnailDownloads()) { + $queue = \Drupal::queue('media_entity_thumbnail'); + $queue->createItem(['id' => $this->id()]); + } + } + + /** + * {@inheritdoc} + */ + public function automaticallySetThumbnail() { + /** @var \Drupal\Core\Entity\EntityStorageInterface $file_storage */ + $file_storage = $this->entityTypeManager()->getStorage('file'); + + // If thumbnail fetching should be queued then temporary use default + // thumbnail or fetch it immediately otherwise. + if ($this->bundle->entity->getQueueThumbnailDownloads() && $this->isNew()) { + $thumbnail_uri = $this->getType()->getDefaultThumbnail(); + } + else { + $thumbnail_uri = $this->getType()->thumbnail($this); + } + $existing = $file_storage->getQuery() + ->condition('uri', $thumbnail_uri) + ->execute(); + + if ($existing) { + $this->thumbnail->target_id = reset($existing); + } + else { + /** @var \Drupal\file\FileInterface $file */ + $file = $file_storage->create(['uri' => $thumbnail_uri]); + if ($owner = $this->getOwner()) { + $file->setOwner($owner); + } + $file->setPermanent(); + $file->save(); + $this->thumbnail->target_id = $file->id(); + } + + // TODO - We should probably use something smarter (tokens, ...). + $this->thumbnail->alt = t('Thumbnail'); + $this->thumbnail->title = $this->label(); + } + + /** + * {@inheritdoc} + */ + public function preSaveRevision(EntityStorageInterface $storage, \stdClass $record) { + parent::preSaveRevision($storage, $record); + + if (!$this->isNewRevision() && isset($this->original) && (!isset($record->revision_log) || $record->revision_log === '')) { + // If we are updating an existing node without adding a new revision, we + // need to make sure $entity->revision_log is reset whenever it is empty. + // Therefore, this code allows us to avoid clobbering an existing log + // entry with an empty one. + $record->revision_log = $this->original->revision_log->value; + } + + if ($this->isNewRevision()) { + $record->revision_timestamp = \Drupal::time()->getRequestTime(); + } + } + + /** + * {@inheritdoc} + */ + public function validate() { + $this->getType()->attachConstraints($this); + return parent::validate(); + } + + /** + * {@inheritdoc} + */ + public static function baseFieldDefinitions(EntityTypeInterface $entity_type) { + $fields = parent::baseFieldDefinitions($entity_type); + $fields += static::publishedBaseFieldDefinitions($entity_type); + + $fields['name'] = BaseFieldDefinition::create('string') + ->setLabel(t('Media name')) + ->setDescription(t('The name of this media.')) + ->setRequired(TRUE) + ->setTranslatable(TRUE) + ->setRevisionable(TRUE) + ->setDefaultValue('') + ->setSetting('max_length', 255) + ->setDisplayOptions('form', [ + 'type' => 'string_textfield', + 'weight' => -5, + ]) + ->setDisplayConfigurable('form', TRUE) + ->setDisplayOptions('view', [ + 'label' => 'hidden', + 'type' => 'string', + 'weight' => -5, + ]); + + $fields['thumbnail'] = BaseFieldDefinition::create('image') + ->setLabel(t('Thumbnail')) + ->setDescription(t('The thumbnail of the media.')) + ->setRevisionable(TRUE) + ->setDisplayOptions('view', [ + 'type' => 'image', + 'weight' => 5, + 'label' => 'hidden', + 'settings' => [ + 'image_style' => 'thumbnail', + ], + ]) + ->setDisplayConfigurable('view', TRUE) + ->setReadOnly(TRUE); + + $fields['uid'] = BaseFieldDefinition::create('entity_reference') + ->setLabel(t('Publisher ID')) + ->setDescription(t('The user ID of the media publisher.')) + ->setRevisionable(TRUE) + ->setDefaultValueCallback('Drupal\media_entity\Entity\Media::getCurrentUserId') + ->setSetting('target_type', 'user') + ->setTranslatable(TRUE) + ->setDisplayOptions('form', [ + 'type' => 'entity_reference_autocomplete', + 'weight' => 5, + 'settings' => [ + 'match_operator' => 'CONTAINS', + 'size' => '60', + 'autocomplete_type' => 'tags', + 'placeholder' => '', + ], + ]) + ->setDisplayConfigurable('form', TRUE) + ->setDisplayOptions('view', [ + 'label' => 'hidden', + 'type' => 'author', + 'weight' => 0, + ]) + ->setDisplayConfigurable('view', TRUE); + + $fields['created'] = BaseFieldDefinition::create('created') + ->setLabel(t('Created')) + ->setDescription(t('The time that the media was created.')) + ->setTranslatable(TRUE) + ->setRevisionable(TRUE) + ->setDisplayOptions('form', [ + 'type' => 'datetime_timestamp', + 'weight' => 10, + ]) + ->setDisplayConfigurable('form', TRUE) + ->setDisplayOptions('view', [ + 'label' => 'hidden', + 'type' => 'timestamp', + 'weight' => 0, + ]) + ->setDisplayConfigurable('view', TRUE); + + $fields['changed'] = BaseFieldDefinition::create('changed') + ->setLabel(t('Changed')) + ->setDescription(t('The time that the media was last edited.')) + ->setTranslatable(TRUE) + ->setRevisionable(TRUE); + + $fields['revision_timestamp'] = BaseFieldDefinition::create('created') + ->setLabel(t('Revision timestamp')) + ->setDescription(t('The time that the current revision was created.')) + ->setQueryable(FALSE) + ->setRevisionable(TRUE); + + $fields['revision_uid'] = BaseFieldDefinition::create('entity_reference') + ->setLabel(t('Revision publisher ID')) + ->setDescription(t('The user ID of the publisher of the current revision.')) + ->setDefaultValueCallback('Drupal\media_entity\Entity\Media::getCurrentUserId') + ->setSetting('target_type', 'user') + ->setQueryable(FALSE) + ->setRevisionable(TRUE); + + $fields['revision_log'] = BaseFieldDefinition::create('string_long') + ->setLabel(t('Revision Log')) + ->setDescription(t('The log entry explaining the changes in this revision.')) + ->setRevisionable(TRUE) + ->setTranslatable(TRUE); + + return $fields; + } + + /** + * Default value callback for 'uid' base field definition. + * + * @see ::baseFieldDefinitions() + * + * @return array + * An array of default values. + */ + public static function getCurrentUserId() { + return [\Drupal::currentUser()->id()]; + } + + /** + * {@inheritdoc} + */ + public function getRevisionCreationTime() { + return $this->revision_timestamp->value; + } + + /** + * {@inheritdoc} + */ + public function setRevisionCreationTime($timestamp) { + $this->revision_timestamp->value = $timestamp; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getRevisionUser() { + return $this->revision_uid->entity; + } + + /** + * {@inheritdoc} + */ + public function setRevisionUser(UserInterface $account) { + $this->revision_uid->entity = $account; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getRevisionUserId() { + return $this->revision_uid->target_id; + } + + /** + * {@inheritdoc} + */ + public function setRevisionUserId($user_id) { + $this->revision_uid->target_id = $user_id; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getRevisionLogMessage() { + return $this->revision_log->value; + } + + /** + * {@inheritdoc} + */ + public function setRevisionLogMessage($revision_log_message) { + $this->revision_log->value = $revision_log_message; + return $this; + } + +} diff --git a/core/modules/media_entity/src/Entity/MediaBundle.php b/core/modules/media_entity/src/Entity/MediaBundle.php new file mode 100644 index 0000000000000000000000000000000000000000..683ab84f521017e13ab4d3ab245c27a2ebb6b4ac --- /dev/null +++ b/core/modules/media_entity/src/Entity/MediaBundle.php @@ -0,0 +1,243 @@ + $this->typePluginCollection(), + ]; + } + + /** + * {@inheritdoc} + */ + public static function getLabel(MediaInterface $media) { + $bundle = static::load($media->bundle()); + return $bundle ? $bundle->label() : FALSE; + } + + /** + * {@inheritdoc} + */ + public static function exists($id) { + return (bool) static::load($id); + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + return $this->description; + } + + /** + * {@inheritdoc} + */ + public function setDescription($description) { + $this->description = $description; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getTypeConfiguration() { + return $this->type_configuration; + } + + /** + * {@inheritdoc} + */ + public function setTypeConfiguration($configuration) { + $this->type_configuration = $configuration; + $this->typePluginCollection = NULL; + } + + /** + * {@inheritdoc} + */ + public function getQueueThumbnailDownloads() { + return $this->queue_thumbnail_downloads; + } + + /** + * {@inheritdoc} + */ + public function setQueueThumbnailDownloads($queue_thumbnail_downloads) { + $this->queue_thumbnail_downloads = $queue_thumbnail_downloads; + } + + /** + * {@inheritdoc} + */ + public function getType() { + return $this->typePluginCollection()->get($this->type); + } + + /** + * Returns type lazy plugin collection. + * + * @return \Drupal\Core\Plugin\DefaultSingleLazyPluginCollection + * The tag plugin collection. + */ + protected function typePluginCollection() { + if (!$this->typePluginCollection) { + $this->typePluginCollection = new DefaultSingleLazyPluginCollection(\Drupal::service('plugin.manager.media_entity.type'), $this->type, $this->type_configuration); + } + return $this->typePluginCollection; + } + + /** + * {@inheritdoc} + */ + public function getStatus() { + return $this->status; + } + + /** + * {@inheritdoc} + */ + public function shouldCreateNewRevision() { + return $this->new_revision; + } + + /** + * {@inheritdoc} + */ + public function setNewRevision($new_revision) { + $this->new_revision = $new_revision; + } + + /** + * {@inheritdoc} + */ + public function getFieldMap() { + return $this->field_map; + } + +} diff --git a/core/modules/media_entity/src/Form/DeleteMultiple.php b/core/modules/media_entity/src/Form/DeleteMultiple.php new file mode 100644 index 0000000000000000000000000000000000000000..2a9cc40ae82e8b4be3b9ad5c005fc18d7cc0efb8 --- /dev/null +++ b/core/modules/media_entity/src/Form/DeleteMultiple.php @@ -0,0 +1,199 @@ +tempStoreFactory = $temp_store_factory; + $this->storage = $manager->getStorage('media'); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('user.private_tempstore'), + $container->get('entity_type.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function getFormId() { + return 'media_multiple_delete_confirm'; + } + + /** + * {@inheritdoc} + */ + public function getQuestion() { + return $this->formatPlural(count($this->entityInfo), 'Are you sure you want to delete this item?', 'Are you sure you want to delete these items?'); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + return new Url('system.admin_content'); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return $this->t('Delete'); + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $this->entityInfo = $this->tempStoreFactory->get('media_multiple_delete_confirm')->get(\Drupal::currentUser()->id()); + if (empty($this->entityInfo)) { + return new RedirectResponse($this->getCancelUrl()->setAbsolute()->toString()); + } + /** @var \Drupal\media_entity\MediaInterface[] $entities */ + $entities = $this->storage->loadMultiple(array_keys($this->entityInfo)); + + $items = []; + foreach ($this->entityInfo as $id => $langcodes) { + foreach ($langcodes as $langcode) { + $entity = $entities[$id]->getTranslation($langcode); + $key = $id . ':' . $langcode; + $default_key = $id . ':' . $entity->getUntranslated()->language()->getId(); + + // If we have a translated entity we build a nested list of translations + // that will be deleted. + $languages = $entity->getTranslationLanguages(); + if (count($languages) > 1 && $entity->isDefaultTranslation()) { + $names = []; + foreach ($languages as $translation_langcode => $language) { + $names[] = $language->getName(); + unset($items[$id . ':' . $translation_langcode]); + } + $items[$default_key] = [ + 'label' => [ + '#markup' => $this->t('@label (Original translation) - The following media translations will be deleted:', ['@label' => $entity->label()]), + ], + 'deleted_translations' => [ + '#theme' => 'item_list', + '#items' => $names, + ], + ]; + } + elseif (!isset($items[$default_key])) { + $items[$key] = $entity->label(); + } + } + } + + $form['entities'] = [ + '#theme' => 'item_list', + '#items' => $items, + ]; + $form = parent::buildForm($form, $form_state); + + return $form; + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + if ($form_state->getValue('confirm') && !empty($this->entityInfo)) { + $total_count = 0; + $delete_entities = []; + /** @var \Drupal\Core\Entity\ContentEntityInterface[][] $delete_translations */ + $delete_translations = []; + /** @var \Drupal\media_entity\MediaInterface[] $entities */ + $entities = $this->storage->loadMultiple(array_keys($this->entityInfo)); + + foreach ($this->entityInfo as $id => $langcodes) { + foreach ($langcodes as $langcode) { + $entity = $entities[$id]->getTranslation($langcode); + if ($entity->isDefaultTranslation()) { + $delete_entities[$id] = $entity; + unset($delete_translations[$id]); + $total_count += count($entity->getTranslationLanguages()); + } + elseif (!isset($delete_entities[$id])) { + $delete_translations[$id][] = $entity; + } + } + } + + if ($delete_entities) { + $this->storage->delete($delete_entities); + $this->logger('media_entity')->notice('Deleted @count media entities.', ['@count' => count($delete_entities)]); + } + + if ($delete_translations) { + $count = 0; + foreach ($delete_translations as $id => $translations) { + $entity = $entities[$id]->getUntranslated(); + foreach ($translations as $translation) { + $entity->removeTranslation($translation->language()->getId()); + } + $entity->save(); + $count += count($translations); + } + if ($count) { + $total_count += $count; + $this->logger('media_entity')->notice('Deleted @count media translations.', ['@count' => $count]); + } + } + + if ($total_count) { + drupal_set_message($this->formatPlural($total_count, 'Deleted 1 media entity.', 'Deleted @count media entities.')); + } + + $this->tempStoreFactory->get('media_multiple_delete_confirm')->delete(\Drupal::currentUser()->id()); + } + + $form_state->setRedirect('system.admin_content'); + } + +} diff --git a/core/modules/media_entity/src/Form/MediaBundleDeleteConfirm.php b/core/modules/media_entity/src/Form/MediaBundleDeleteConfirm.php new file mode 100644 index 0000000000000000000000000000000000000000..c56318f1ea88e1611aec6a1f22a8c5561cd1f7c0 --- /dev/null +++ b/core/modules/media_entity/src/Form/MediaBundleDeleteConfirm.php @@ -0,0 +1,62 @@ +queryFactory = $query_factory; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity.query') + ); + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $num_entities = $this->queryFactory->get('media') + ->condition('bundle', $this->entity->id()) + ->count() + ->execute(); + if ($num_entities) { + $caption = '

' . $this->formatPlural($num_entities, + '%type is used by @count piece of content on your site. You can not remove this content type until you have removed all of the %type content.', + '%type is used by @count pieces of content on your site. You may not remove %type until you have removed all of the %type content.', + ['%type' => $this->entity->label()]) . '

'; + $form['#title'] = $this->getQuestion(); + $form['description'] = ['#markup' => $caption]; + return $form; + } + + return parent::buildForm($form, $form_state); + } + +} diff --git a/core/modules/media_entity/src/Form/MediaDeleteForm.php b/core/modules/media_entity/src/Form/MediaDeleteForm.php new file mode 100644 index 0000000000000000000000000000000000000000..d84e34a17f9fc187ccf89c0a52eb3f4719180564 --- /dev/null +++ b/core/modules/media_entity/src/Form/MediaDeleteForm.php @@ -0,0 +1,20 @@ +hasPermission('administer media')) { + return AccessResult::allowed()->cachePerPermissions(); + } + + $is_owner = ($account->id() && $account->id() == $entity->getPublisherId()) ? TRUE : FALSE; + switch ($operation) { + case 'view': + return AccessResult::allowedIf($account->hasPermission('view media') && $entity->status->value)->cachePerPermissions()->addCacheableDependency($entity); + + case 'update': + return AccessResult::allowedIf(($account->hasPermission('update media') && $is_owner) || $account->hasPermission('update any media'))->cachePerPermissions()->cachePerUser()->addCacheableDependency($entity); + + case 'delete': + return AccessResult::allowedIf(($account->hasPermission('delete media') && $is_owner) || $account->hasPermission('delete any media'))->cachePerPermissions()->cachePerUser()->addCacheableDependency($entity); + } + + // No opinion. + return AccessResult::neutral()->cachePerPermissions(); + } + + /** + * {@inheritdoc} + */ + protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) { + return AccessResult::allowedIfHasPermission($account, 'create media'); + } + +} diff --git a/core/modules/media_entity/src/MediaBundleForm.php b/core/modules/media_entity/src/MediaBundleForm.php new file mode 100644 index 0000000000000000000000000000000000000000..6ecad543c5145e6248d14533c4545fb0cd516338 --- /dev/null +++ b/core/modules/media_entity/src/MediaBundleForm.php @@ -0,0 +1,385 @@ +mediaTypeManager = $media_type_manager; + $this->entityFieldManager = $entity_field_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('plugin.manager.media_entity.type'), + $container->get('entity_field.manager') + ); + } + + /** + * Ajax callback triggered by the type provider select element. + * + * @param array $form + * The form array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * Current form state. + * + * @return \Drupal\Core\Ajax\AjaxResponse + * The ajax response. + */ + public function ajaxTypeProviderData(array $form, FormStateInterface $form_state) { + $response = new AjaxResponse(); + $plugin = $this->entity->getType()->getPluginId(); + + $response->addCommand(new ReplaceCommand('#edit-type-configuration-plugin-wrapper', $form['type_configuration'][$plugin])); + $response->addCommand(new ReplaceCommand('#field-mapping-wrapper', $form['field_mapping'])); + + return $response; + } + + /** + * {@inheritdoc} + */ + public function form(array $form, FormStateInterface $form_state) { + $form = parent::form($form, $form_state); + + /** @var \Drupal\media_entity\MediaBundleInterface $bundle */ + $form['#entity'] = $bundle = $this->getEntity(); + $form_state->set('bundle', $bundle->id()); + + if ($this->operation == 'add') { + $form['#title'] = $this->t('Add media bundle'); + } + elseif ($this->operation == 'edit') { + $form['#title'] = $this->t('Edit %label media bundle', ['%label' => $bundle->label()]); + } + + $form['label'] = [ + '#title' => $this->t('Label'), + '#type' => 'textfield', + '#default_value' => $bundle->label(), + '#description' => $this->t('The human-readable name of this media bundle.'), + '#required' => TRUE, + '#size' => 30, + '#weight' => -100, + ]; + + // @todo: '#disabled' not always FALSE. + $form['id'] = [ + '#type' => 'machine_name', + '#default_value' => $bundle->id(), + '#maxlength' => 32, + '#disabled' => !$bundle->isNew(), + '#machine_name' => [ + 'exists' => [MediaBundle::class, 'exists'], + 'source' => ['label'], + ], + '#description' => $this->t('A unique machine-readable name for this media bundle.'), + '#weight' => -90, + ]; + + $form['description'] = [ + '#title' => $this->t('Description'), + '#type' => 'textarea', + '#default_value' => $bundle->getDescription(), + '#description' => $this->t('Describe this media bundle. The text will be displayed on the Add new media page.'), + '#weight' => -80, + ]; + + $plugins = $this->mediaTypeManager->getDefinitions(); + $options = []; + foreach ($plugins as $plugin => $definition) { + $options[$plugin] = $definition['label']; + } + + $form['type'] = [ + '#type' => 'select', + '#title' => $this->t('Type provider'), + '#default_value' => $bundle->getType()->getPluginId(), + '#options' => $options, + '#description' => $this->t('Media type provider plugin that is responsible for additional logic related to this media.'), + '#weight' => -70, + '#ajax' => [ + 'callback' => '::ajaxTypeProviderData', + 'progress' => [ + 'type' => 'throbber', + 'message' => $this->t('Updating type provider configuration form.'), + ], + ], + ]; + + // Media type plugin configuration. + $form['type_configuration'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Type provider configuration'), + '#tree' => TRUE, + '#weight' => -60, + ]; + + /** @var \Drupal\media_entity\MediaTypeInterface $plugin */ + if ($plugin = $bundle->getType()) { + $plugin_configuration = (empty($this->configurableInstances[$plugin->getPluginId()]['plugin_config'])) ? $bundle->getTypeConfiguration() : $this->configurableInstances[$plugin->getPluginId()]['plugin_config']; + /** @var \Drupal\media_entity\MediaTypeBase $instance */ + $instance = $this->mediaTypeManager->createInstance($plugin->getPluginId(), $plugin_configuration); + // Store the configuration for validate and submit handlers. + $this->configurableInstances[$plugin->getPluginId()]['plugin_config'] = $plugin_configuration; + + $form['type_configuration'][$plugin->getPluginId()] = [ + '#type' => 'container', + '#attributes' => [ + 'id' => 'edit-type-configuration-plugin-wrapper', + ], + ]; + $form['type_configuration'][$plugin->getPluginId()] += $instance->buildConfigurationForm([], $form_state); + } + + // Field mapping configuration. + $form['field_mapping'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Field mapping'), + '#tree' => TRUE, + '#attributes' => ['id' => 'field-mapping-wrapper'], + 'description' => [ + '#type' => 'html_tag', + '#tag' => 'p', + '#value' => $this->t('Media type plugins can provide metadata fields such as title, caption, size information, credits, ... Media entity can automatically save this metadata information to entity fields, which can be configured below. Information will only be mapped if the entity field is empty.'), + ], + '#weight' => -50, + ]; + + if (empty($plugin) || empty($plugin->providedFields())) { + $form['field_mapping']['empty_message'] = [ + '#prefix' => '', + '#suffix' => '', + '#markup' => $this->t('No metadata fields available.'), + ]; + } + else { + $skipped_fields = [ + 'mid', + 'uuid', + 'vid', + 'bundle', + 'langcode', + 'default_langcode', + 'uid', + 'revision_timestamp', + 'revision_log', + 'revision_uid', + ]; + $options = ['_none' => $this->t('- Skip field -')]; + foreach ($this->entityFieldManager->getFieldDefinitions('media', $bundle->id()) as $field_name => $field) { + if (!in_array($field_name, $skipped_fields)) { + $options[$field_name] = $field->getLabel(); + } + } + + $field_map = $bundle->getFieldMap(); + foreach ($plugin->providedFields() as $field_name => $field_label) { + $form['field_mapping'][$field_name] = [ + '#type' => 'select', + '#title' => $field_label, + '#options' => $options, + '#default_value' => isset($field_map[$field_name]) ? $field_map[$field_name] : '_none', + ]; + } + } + + $form['additional_settings'] = [ + '#type' => 'vertical_tabs', + '#attached' => [ + 'library' => ['media_entity/media_bundle_form'], + ], + '#weight' => 100, + ]; + + $form['workflow'] = [ + '#type' => 'details', + '#title' => $this->t('Publishing options'), + '#group' => 'additional_settings', + ]; + + $workflow_options = [ + 'status' => $bundle->getStatus(), + 'new_revision' => $bundle->shouldCreateNewRevision(), + 'queue_thumbnail_downloads' => $bundle->getQueueThumbnailDownloads(), + ]; + // Prepare workflow options to be used for 'checkboxes' form element. + $keys = array_keys(array_filter($workflow_options)); + $workflow_options = array_combine($keys, $keys); + $form['workflow']['options'] = [ + '#type' => 'checkboxes', + '#title' => $this->t('Default options'), + '#default_value' => $workflow_options, + '#options' => [ + 'status' => $this->t('Published'), + 'new_revision' => $this->t('Create new revision'), + 'queue_thumbnail_downloads' => $this->t('Queue thumbnail downloads'), + ], + ]; + + $form['workflow']['options']['status']['#description'] = $this->t('Entities will be automatically published when they are created.'); + $form['workflow']['options']['new_revision']['#description'] = $this->t('Automatically create a new revision of media entities. Users with the Administer media permission will be able to override this option.'); + $form['workflow']['options']['queue_thumbnail_downloads']['#description'] = $this->t('Download thumbnails via a queue.'); + + if ($this->moduleHandler->moduleExists('language')) { + $form['language'] = [ + '#type' => 'details', + '#title' => $this->t('Language settings'), + '#group' => 'additional_settings', + ]; + + $language_configuration = ContentLanguageSettings::loadByEntityTypeBundle('media', $bundle->id()); + + $form['language']['language_configuration'] = [ + '#type' => 'language_configuration', + '#entity_information' => [ + 'entity_type' => 'media', + 'bundle' => $bundle->id(), + ], + '#default_value' => $language_configuration, + ]; + } + + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + parent::validateForm($form, $form_state); + + // Let the selected plugin validate its settings. + $plugin = $this->entity->getType()->getPluginId(); + $plugin_configuration = !empty($this->configurableInstances[$plugin]['plugin_config']) ? $this->configurableInstances[$plugin]['plugin_config'] : []; + $instance = $this->mediaTypeManager->createInstance($plugin, $plugin_configuration); + $instance->validateConfigurationForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + parent::submitForm($form, $form_state); + + $this->entity->setQueueThumbnailDownloads((bool) $form_state->getValue(['options', 'queue_thumbnail_downloads'])); + $this->entity->setStatus((bool) $form_state->getValue(['options', 'status'])); + + $this->entity->setNewRevision((bool) $form_state->getValue(['options', 'new_revision'])); + + // Let the selected plugin save its settings. + $plugin = $this->entity->getType()->getPluginId(); + $plugin_configuration = !empty($this->configurableInstances[$plugin]['plugin_config']) ? $this->configurableInstances[$plugin]['plugin_config'] : []; + $instance = $this->mediaTypeManager->createInstance($plugin, $plugin_configuration); + $instance->submitConfigurationForm($form, $form_state); + } + + /** + * {@inheritdoc} + */ + protected function actions(array $form, FormStateInterface $form_state) { + $actions = parent::actions($form, $form_state); + $actions['submit']['#value'] = $this->t('Save media bundle'); + $actions['delete']['#value'] = $this->t('Delete media bundle'); + $actions['delete']['#access'] = $this->entity->access('delete'); + return $actions; + } + + /** + * {@inheritdoc} + */ + protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) { + $configuration = $form_state->getValue('type_configuration'); + + // Store previous plugin config. + $plugin = $entity->getType()->getPluginId(); + $this->configurableInstances[$plugin]['plugin_config'] = empty($configuration[$plugin]) ? [] : $configuration[$plugin]; + + /** @var \Drupal\media_entity\MediaBundleInterface $entity */ + parent::copyFormValuesToEntity($entity, $form, $form_state); + + // Use type configuration for the plugin that was chosen. + $plugin = $entity->getType()->getPluginId(); + $plugin_configuration = empty($configuration[$plugin]) ? [] : $configuration[$plugin]; + $entity->set('type_configuration', $plugin_configuration); + + // Save field mapping. + $entity->set('field_map', array_filter( + $form_state->getValue('field_mapping', []), + function ($item) { return $item != '_none'; } + )); + } + + /** + * {@inheritdoc} + */ + public function save(array $form, FormStateInterface $form_state) { + /** @var \Drupal\media_entity\MediaBundleInterface $bundle */ + $bundle = $this->entity; + $status = $bundle->save(); + + $t_args = ['%name' => $bundle->label()]; + if ($status == SAVED_UPDATED) { + drupal_set_message($this->t('The media bundle %name has been updated.', $t_args)); + } + elseif ($status == SAVED_NEW) { + drupal_set_message($this->t('The media bundle %name has been added.', $t_args)); + $this->logger('media')->notice('Added bundle %name.', $t_args); + } + + // Override the "status" base field default value, for this bundle. + $fields = $this->entityFieldManager->getFieldDefinitions('media', $bundle->id()); + $media = $this->entityTypeManager->getStorage('media')->create(array('bundle' => $bundle->id())); + $value = (bool) $form_state->getValue(['options', 'status']); + if ($media->status->value != $value) { + $fields['status']->getConfig($bundle->id())->setDefaultValue($value)->save(); + } + + $form_state->setRedirectUrl($bundle->toUrl('collection')); + } + +} diff --git a/core/modules/media_entity/src/MediaBundleInterface.php b/core/modules/media_entity/src/MediaBundleInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..6a2332cecaee359ea5dc71e73ec290730669a845 --- /dev/null +++ b/core/modules/media_entity/src/MediaBundleInterface.php @@ -0,0 +1,115 @@ +t('Name'); + $header['description'] = [ + 'data' => $this->t('Description'), + 'class' => [RESPONSIVE_PRIORITY_MEDIUM], + ]; + return $header + parent::buildHeader(); + } + + /** + * {@inheritdoc} + */ + public function buildRow(EntityInterface $entity) { + $row['title'] = [ + 'data' => $entity->label(), + 'class' => ['menu-label'], + ]; + $row['description'] = Xss::filterAdmin($entity->getDescription()); + return $row + parent::buildRow($entity); + } + + /** + * {@inheritdoc} + */ + public function render() { + $build = parent::render(); + $build['#empty'] = $this->t('No media bundle available. Add media bundle.', [ + '@link' => Url::fromRoute('entity.media_bundle.add_form')->toString(), + ]); + return $build; + } + +} diff --git a/core/modules/media_entity/src/MediaForm.php b/core/modules/media_entity/src/MediaForm.php new file mode 100644 index 0000000000000000000000000000000000000000..6d77f997980ee1ae90f4bfb907ccf8ee99faeee1 --- /dev/null +++ b/core/modules/media_entity/src/MediaForm.php @@ -0,0 +1,242 @@ +getEntity()->isNew()) { + $this->entity->setRevisionLogMessage(NULL); + $this->entity->setOwnerId($this->currentUser()->id()); + $this->entity->setCreatedTime(\Drupal::time()->getRequestTime()); + } + } + + /** + * {@inheritdoc} + */ + public function form(array $form, FormStateInterface $form_state) { + $form = parent::form($form, $form_state); + $entity_type = $this->getEntity()->getEntityType(); + /** @var \Drupal\media_entity\Entity\MediaBundle $bundle_entity */ + $bundle_entity = $this->entity->bundle->entity; + $account = $this->currentUser(); + $new_revision = $this->entity->bundle->entity->shouldCreateNewRevision(); + + if ($this->operation == 'edit') { + $form['#title'] = $this->t('Edit %bundle_label @label', [ + '%bundle_label' => $bundle_entity->label(), + '@label' => $this->entity->label(), + ]); + } + + $form['advanced'] = [ + '#type' => 'vertical_tabs', + '#weight' => 99, + ]; + + // Add a log field if the "Create new revision" option is checked, or if the + // current user has the ability to check that option. + $form['revision_information'] = [ + '#type' => 'details', + '#title' => $this->t('Revision information'), + // Open by default when "Create new revision" is checked. + '#open' => $new_revision, + '#group' => 'advanced', + '#weight' => 20, + '#access' => $new_revision || $account->hasPermission($entity_type->get('admin_permission')), + '#attributes' => [ + 'class' => ['media-form-revision-information'], + ], + ]; + + $form['revision_information']['revision'] = [ + '#type' => 'checkbox', + '#title' => $this->t('Create new revision'), + '#default_value' => $new_revision, + '#access' => $account->hasPermission($entity_type->get('admin_permission')), + ]; + + // Check the revision log checkbox when the log textarea is filled in. + // This must not happen if "Create new revision" is enabled by default, + // since the state would auto-disable the checkbox otherwise. + if (!$new_revision) { + $form['revision_information']['revision']['#states'] = [ + 'checked' => [ + 'textarea[name="revision_log"]' => ['empty' => FALSE], + ], + ]; + } + + $form['revision_information']['revision_log'] = [ + '#type' => 'textarea', + '#title' => $this->t('Revision log message'), + '#rows' => 4, + '#default_value' => $this->entity->getRevisionLogMessage(), + '#description' => $this->t('Briefly describe the changes you have made.'), + ]; + + // Media author information for administrators. + if (isset($form['uid']) || isset($form['created'])) { + $form['author'] = [ + '#type' => 'details', + '#title' => $this->t('Authoring information'), + '#group' => 'advanced', + '#attributes' => [ + 'class' => ['media-form-author'], + ], + '#weight' => 90, + '#optional' => TRUE, + ]; + } + + if (isset($form['uid'])) { + $form['uid']['#group'] = 'author'; + } + + if (isset($form['created'])) { + $form['created']['#group'] = 'author'; + } + + $form['#attached']['library'][] = 'media_entity/media_form'; + + $form['#entity_builders']['update_status'] = [$this, 'updateStatus']; + + return $form; + } + + /** + * {@inheritdoc} + */ + protected function actions(array $form, FormStateInterface $form_state) { + $element = parent::actions($form, $form_state); + $media = $this->entity; + + // Add a "Publish" button. + $element['publish'] = $element['submit']; + // If the "Publish" button is clicked, we want to update the status to + // "published". + $element['publish']['#published_status'] = TRUE; + $element['publish']['#dropbutton'] = 'save'; + if ($media->isNew()) { + $element['publish']['#value'] = $this->t('Save and publish'); + } + else { + $element['publish']['#value'] = $media->isPublished() ? $this->t('Save and keep published') : $this->t('Save and publish'); + } + $element['publish']['#weight'] = 0; + + // Add a "Unpublish" button. + $element['unpublish'] = $element['submit']; + // If the "Unpublish" button is clicked, we want to update the status to + // "unpublished". + $element['unpublish']['#published_status'] = FALSE; + $element['unpublish']['#dropbutton'] = 'save'; + if ($media->isNew()) { + $element['unpublish']['#value'] = $this->t('Save as unpublished'); + } + else { + $element['unpublish']['#value'] = !$media->isPublished() ? $this->t('Save and keep unpublished') : $this->t('Save and unpublish'); + } + $element['unpublish']['#weight'] = 10; + + // If already published, the 'publish' button is primary. + if ($media->isPublished()) { + unset($element['unpublish']['#button_type']); + } + // Otherwise, the 'unpublish' button is primary and should come first. + else { + unset($element['publish']['#button_type']); + $element['unpublish']['#weight'] = -10; + } + + // Remove the "Save" button. + $element['submit']['#access'] = FALSE; + + $element['delete']['#access'] = $media->access('delete'); + $element['delete']['#weight'] = 100; + + return $element; + } + + /** + * Entity builder updating the media status with the submitted value. + * + * @param string $entity_type_id + * The entity type identifier. + * @param \Drupal\media_entity\MediaInterface $media + * The media updated with the submitted values. + * @param array $form + * The complete form array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @see \Drupal\media_entity\MediaForm::form() + */ + public function updateStatus($entity_type_id, MediaInterface $media, array $form, FormStateInterface $form_state) { + $element = $form_state->getTriggeringElement(); + if (isset($element['#published_status'])) { + if ((bool) $element['#published_status']) { + $media->setPublished(); + } + else { + $media->setUnpublished(); + } + } + } + + /** + * {@inheritdoc} + */ + public function save(array $form, FormStateInterface $form_state) { + // Save as a new revision if requested to do so. + if (!$form_state->isValueEmpty('revision')) { + $this->entity->setNewRevision(); + $this->entity->setRevisionUserId(\Drupal::currentUser()->id()); + } + + $insert = $this->entity->isNew(); + $this->entity->save(); + $context = ['@type' => $this->entity->bundle(), '%info' => $this->entity->label()]; + $logger = $this->logger($this->entity->id()); + $t_args = ['@type' => $this->entity->bundle->entity->label(), '%info' => $this->entity->label()]; + + if ($insert) { + $logger->notice('@type: added %info.', $context); + drupal_set_message($this->t('@type %info has been created.', $t_args)); + } + else { + $logger->notice('@type: updated %info.', $context); + drupal_set_message($this->t('@type %info has been updated.', $t_args)); + } + + $form_state->setRedirectUrl($this->entity->toUrl('canonical')); + } + +} diff --git a/core/modules/media_entity/src/MediaInterface.php b/core/modules/media_entity/src/MediaInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..fed1a65656f757bbb876e5a92245ada68424596d --- /dev/null +++ b/core/modules/media_entity/src/MediaInterface.php @@ -0,0 +1,87 @@ +entityTypeManager = $entity_type_manager; + $this->entityFieldManager = $entity_field_manager; + $this->configFactory = $config_factory; + $this->setConfiguration($configuration); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_type.manager'), + $container->get('entity_field.manager'), + $container->get('config.factory') + ); + } + + /** + * {@inheritdoc} + */ + public function setConfiguration(array $configuration) { + $this->configuration = NestedArray::mergeDeep( + $this->defaultConfiguration(), + $configuration + ); + } + + /** + * {@inheritdoc} + */ + public function getConfiguration() { + return $this->configuration; + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return []; + } + + /** + * {@inheritdoc} + */ + public function getDefaultThumbnail() { + return $this->configFactory->get('media_entity.settings')->get('icon_base') . '/generic.png'; + } + + /** + * {@inheritdoc} + */ + public function label() { + return $this->label; + } + + /** + * {@inheritdoc} + */ + public function attachConstraints(MediaInterface $media) {} + + /** + * {@inheritdoc} + */ + public function calculateDependencies() { + return []; + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + return []; + } + + /** + * {@inheritdoc} + */ + public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {} + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {} + + /** + * {@inheritdoc} + */ + public function getDefaultName(MediaInterface $media) { + return 'media:' . $media->bundle() . ':' . $media->uuid(); + } + +} diff --git a/core/modules/media_entity/src/MediaTypeInterface.php b/core/modules/media_entity/src/MediaTypeInterface.php new file mode 100644 index 0000000000000000000000000000000000000000..2a171c2c60906e8e6a638ce9b1aea65fa183adb6 --- /dev/null +++ b/core/modules/media_entity/src/MediaTypeInterface.php @@ -0,0 +1,88 @@ +alterInfo('media_entity_type_info'); + $this->setCacheBackend($cache_backend, 'media_entity_type_plugins'); + } + +} diff --git a/core/modules/media_entity/src/MediaViewsData.php b/core/modules/media_entity/src/MediaViewsData.php new file mode 100644 index 0000000000000000000000000000000000000000..f1a4e6f38882711f2d446fae46569d4ba8807e94 --- /dev/null +++ b/core/modules/media_entity/src/MediaViewsData.php @@ -0,0 +1,24 @@ +currentUser = $current_user; + $this->tempStore = $temp_store_factory->get('media_multiple_delete_confirm'); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('user.private_tempstore'), + $container->get('current_user') + ); + } + + /** + * {@inheritdoc} + */ + public function executeMultiple(array $entities) { + $info = []; + /** @var \Drupal\media_entity\MediaInterface $media */ + foreach ($entities as $media) { + $langcode = $media->language()->getId(); + $info[$media->id()][$langcode] = $langcode; + } + $this->tempStore->set($this->currentUser->id(), $info); + } + + /** + * {@inheritdoc} + */ + public function execute($object = NULL) { + $this->executeMultiple(array($object)); + } + + /** + * {@inheritdoc} + */ + public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { + /** @var \Drupal\media_entity\MediaInterface $object */ + return $object->access('delete', $account, $return_as_object); + } + +} diff --git a/core/modules/media_entity/src/Plugin/Action/PublishMedia.php b/core/modules/media_entity/src/Plugin/Action/PublishMedia.php new file mode 100644 index 0000000000000000000000000000000000000000..fcea6c30f35fdaee07bb0858559f3a3513e33145 --- /dev/null +++ b/core/modules/media_entity/src/Plugin/Action/PublishMedia.php @@ -0,0 +1,38 @@ +setPublished()->save(); + } + + /** + * {@inheritdoc} + */ + public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { + /** @var \Drupal\media_entity\MediaInterface $object */ + $result = $object->access('update', $account, TRUE) + ->andIf($object->status->access('update', $account, TRUE)); + + return $return_as_object ? $result : $result->isAllowed(); + } + +} diff --git a/core/modules/media_entity/src/Plugin/Action/SaveMedia.php b/core/modules/media_entity/src/Plugin/Action/SaveMedia.php new file mode 100644 index 0000000000000000000000000000000000000000..a061cfc4da1477771bb5c0b481a532fa10628285 --- /dev/null +++ b/core/modules/media_entity/src/Plugin/Action/SaveMedia.php @@ -0,0 +1,37 @@ +changed = 0; + $entity->save(); + } + + /** + * {@inheritdoc} + */ + public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { + /** @var \Drupal\media_entity\MediaInterface $object */ + return $object->access('update', $account, $return_as_object); + } + +} diff --git a/core/modules/media_entity/src/Plugin/Action/UnpublishMedia.php b/core/modules/media_entity/src/Plugin/Action/UnpublishMedia.php new file mode 100644 index 0000000000000000000000000000000000000000..eb9821f61609e76b746a926381ffc05f289d36b8 --- /dev/null +++ b/core/modules/media_entity/src/Plugin/Action/UnpublishMedia.php @@ -0,0 +1,38 @@ +setUnpublished()->save(); + } + + /** + * {@inheritdoc} + */ + public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { + /** @var \Drupal\media_entity\MediaInterface $object */ + $result = $object->access('update', $account, TRUE) + ->andIf($object->status->access('update', $account, TRUE)); + + return $return_as_object ? $result : $result->isAllowed(); + } + +} diff --git a/core/modules/media_entity/src/Plugin/Field/FieldFormatter/MediaThumbnailFormatter.php b/core/modules/media_entity/src/Plugin/Field/FieldFormatter/MediaThumbnailFormatter.php new file mode 100644 index 0000000000000000000000000000000000000000..a94d121762f824868a41a306ddeebfbb49ecebef --- /dev/null +++ b/core/modules/media_entity/src/Plugin/Field/FieldFormatter/MediaThumbnailFormatter.php @@ -0,0 +1,188 @@ +renderer = $renderer; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $plugin_id, + $plugin_definition, + $configuration['field_definition'], + $configuration['settings'], + $configuration['label'], + $configuration['view_mode'], + $configuration['third_party_settings'], + $container->get('current_user'), + $container->get('entity.manager')->getStorage('image_style'), + $container->get('renderer') + ); + } + + /** + * {@inheritdoc} + * + * This has to be overriden because FileFormatterBase expects $item to be + * of type \Drupal\file\Plugin\Field\FieldType\FileItem and calls + * isDisplayed() which is not in FieldItemInterface. + */ + protected function needsEntityLoad(EntityReferenceItem $item) { + return !$item->hasNewEntity(); + } + + /** + * {@inheritdoc} + */ + public function settingsForm(array $form, FormStateInterface $form_state) { + $element = parent::settingsForm($form, $form_state); + + $link_types = [ + 'content' => $this->t('Content'), + 'media' => $this->t('Media entity'), + ]; + $element['image_link']['#options'] = $link_types; + + return $element; + } + + /** + * {@inheritdoc} + */ + public function settingsSummary() { + $summary = parent::settingsSummary(); + + $link_types = [ + 'content' => $this->t('Linked to content'), + 'media' => $this->t('Linked to media entity'), + ]; + // Display this setting only if image is linked. + $image_link_setting = $this->getSetting('image_link'); + if (isset($link_types[$image_link_setting])) { + $summary[] = $link_types[$image_link_setting]; + } + + return $summary; + } + + /** + * {@inheritdoc} + */ + public function viewElements(FieldItemListInterface $items, $langcode) { + $elements = []; + $media = $this->getEntitiesToView($items, $langcode); + + // Early opt-out if the field is empty. + if (empty($media)) { + return $elements; + } + + $url = NULL; + $image_link_setting = $this->getSetting('image_link'); + // Check if the formatter involves a link. + if ($image_link_setting == 'content') { + $entity = $items->getEntity(); + if (!$entity->isNew()) { + $url = $entity->toUrl(); + } + } + elseif ($image_link_setting == 'media') { + $link_media = TRUE; + } + + $image_style_setting = $this->getSetting('image_style'); + + /** @var \Drupal\media_entity\MediaInterface $media_item */ + foreach ($media as $delta => $media_item) { + if (isset($link_media)) { + $url = $media_item->toUrl(); + } + + $elements[$delta] = [ + '#theme' => 'image_formatter', + '#item' => $media_item->get('thumbnail'), + '#item_attributes' => [], + '#image_style' => $image_style_setting, + '#url' => $url, + ]; + + // Collect cache tags to be added for each item in the field. + $this->renderer->addCacheableDependency($elements[$delta], $media_item); + } + + // Collect cache tags related to the image style setting. + if ($image_link_setting && ($image_style = $this->imageStyleStorage->load($image_style_setting))) { + $this->renderer->addCacheableDependency($elements, $image_style); + } + + return $elements; + } + + /** + * {@inheritdoc} + */ + public static function isApplicable(FieldDefinitionInterface $field_definition) { + // This formatter is only available for entity types that reference + // media entities. + $target_type = $field_definition->getFieldStorageDefinition()->getSetting('target_type'); + return $target_type == 'media'; + } + +} diff --git a/core/modules/media_entity/src/Plugin/MediaEntity/Type/Document.php b/core/modules/media_entity/src/Plugin/MediaEntity/Type/Document.php new file mode 100644 index 0000000000000000000000000000000000000000..e68d1552380091088ed4b12e07ca77b572f4cc60 --- /dev/null +++ b/core/modules/media_entity/src/Plugin/MediaEntity/Type/Document.php @@ -0,0 +1,121 @@ + $this->t('MIME type'), + 'size' => $this->t('Size'), + ]; + } + + /** + * {@inheritdoc} + */ + public function getField(MediaInterface $media, $name) { + $source_field = $this->configuration['source_field']; + + // Get the file document. + /** @var \Drupal\file\FileInterface $file */ + $file = $media->{$source_field}->entity; + + // Return the field. + switch ($name) { + case 'mime': + return $file->getMimeType() ?: FALSE; + + case 'size': + $size = $file->getSize(); + return is_numeric($size) ? $size : FALSE; + } + + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + /** @var \Drupal\media_entity\MediaBundleInterface $bundle */ + $bundle = $form_state->getFormObject()->getEntity(); + $options = []; + $allowed_field_types = ['file']; + + /** @var \Drupal\Core\Field\FieldDefinitionInterface $field */ + foreach ($this->entityFieldManager->getFieldDefinitions('media', $bundle->id()) as $field_name => $field) { + if (in_array($field->getType(), $allowed_field_types) && !$field->getFieldStorageDefinition()->isBaseField()) { + $options[$field_name] = $field->getLabel(); + } + } + + $form['source_field'] = [ + '#type' => 'select', + '#title' => $this->t('Field with source information'), + '#description' => $this->t('Field on media entity that stores Document file. You can create a bundle without selecting a value for this dropdown initially. This dropdown can be populated after adding fields to the bundle.'), + '#default_value' => empty($this->configuration['source_field']) ? NULL : $this->configuration['source_field'], + '#options' => $options, + ]; + + return $form; + } + + /** + * {@inheritdoc} + */ + public function thumbnail(MediaInterface $media) { + $source_field = $this->configuration['source_field']; + /** @var \Drupal\file\FileInterface $file */ + $file = $media->{$source_field}->entity; + $icon_base = $this->configFactory->get('media_entity.settings')->get('icon_base'); + $thumbnail = FALSE; + if ($file) { + $mimetype = $file->getMimeType(); + $mimetype = explode('/', $mimetype); + $thumbnail = $icon_base . "/{$mimetype[0]}--{$mimetype[1]}.png"; + + if (!is_file($thumbnail)) { + $thumbnail = $icon_base . "/{$mimetype[1]}.png"; + } + } + + if (!is_file($thumbnail)) { + $thumbnail = $icon_base . '/generic.png'; + } + + return $thumbnail; + } + + /** + * {@inheritdoc} + */ + public function getDefaultName(MediaInterface $media) { + // The default name will be the filename of the source_field, if present. + $source_field = $this->configuration['source_field']; + + /** @var \Drupal\file\FileInterface $file */ + if (!empty($source_field) && ($file = $media->{$source_field}->entity)) { + return $file->getFilename(); + } + + return parent::getDefaultName($media); + } + +} diff --git a/core/modules/media_entity/src/Plugin/MediaEntity/Type/Generic.php b/core/modules/media_entity/src/Plugin/MediaEntity/Type/Generic.php new file mode 100644 index 0000000000000000000000000000000000000000..021fac6dae91f2826eff3ae27b54d978e22b949e --- /dev/null +++ b/core/modules/media_entity/src/Plugin/MediaEntity/Type/Generic.php @@ -0,0 +1,41 @@ +getDefaultThumbnail(); + } + +} diff --git a/core/modules/media_entity/src/Plugin/QueueWorker/ThumbnailDownloader.php b/core/modules/media_entity/src/Plugin/QueueWorker/ThumbnailDownloader.php new file mode 100644 index 0000000000000000000000000000000000000000..165f6ab1a59dea0b761ae1459897cfb4a51ed21e --- /dev/null +++ b/core/modules/media_entity/src/Plugin/QueueWorker/ThumbnailDownloader.php @@ -0,0 +1,30 @@ +automaticallySetThumbnail(); + $entity->save(); + } + } + +} diff --git a/core/modules/media_entity/src/Plugin/views/wizard/Media.php b/core/modules/media_entity/src/Plugin/views/wizard/Media.php new file mode 100644 index 0000000000000000000000000000000000000000..3a2d9da92275586b4bc9607665bb31a3976ba96b --- /dev/null +++ b/core/modules/media_entity/src/Plugin/views/wizard/Media.php @@ -0,0 +1,84 @@ + [ + 'value' => TRUE, + 'table' => 'media_field_data', + 'field' => 'status', + 'plugin_id' => 'boolean', + 'entity_type' => 'media', + 'entity_field' => 'status', + ], + ]; + + /** + * {@inheritdoc} + */ + public function getAvailableSorts() { + return [ + 'media_field_data-name:DESC' => $this->t('Media name'), + ]; + } + + /** + * {@inheritdoc} + */ + protected function defaultDisplayOptions() { + $display_options = parent::defaultDisplayOptions(); + + // Add permission-based access control. + $display_options['access']['type'] = 'perm'; + $display_options['access']['options']['perm'] = 'view media'; + + // Remove the default fields, since we are customizing them here. + unset($display_options['fields']); + + // Add the name field, so that the display has content if the user switches + // to a row style that uses fields. + /* Field: Media: Name */ + $display_options['fields']['name']['id'] = 'name'; + $display_options['fields']['name']['table'] = 'media_field_data'; + $display_options['fields']['name']['field'] = 'name'; + $display_options['fields']['name']['entity_type'] = 'media'; + $display_options['fields']['name']['entity_field'] = 'media'; + $display_options['fields']['name']['label'] = ''; + $display_options['fields']['name']['alter']['alter_text'] = 0; + $display_options['fields']['name']['alter']['make_link'] = 0; + $display_options['fields']['name']['alter']['absolute'] = 0; + $display_options['fields']['name']['alter']['trim'] = 0; + $display_options['fields']['name']['alter']['word_boundary'] = 0; + $display_options['fields']['name']['alter']['ellipsis'] = 0; + $display_options['fields']['name']['alter']['strip_tags'] = 0; + $display_options['fields']['name']['alter']['html'] = 0; + $display_options['fields']['name']['hide_empty'] = 0; + $display_options['fields']['name']['empty_zero'] = 0; + $display_options['fields']['name']['settings']['link_to_entity'] = 1; + $display_options['fields']['name']['plugin_id'] = 'field'; + + return $display_options; + } + +} diff --git a/core/modules/media_entity/src/Plugin/views/wizard/MediaRevision.php b/core/modules/media_entity/src/Plugin/views/wizard/MediaRevision.php new file mode 100644 index 0000000000000000000000000000000000000000..72bceb0996d853474eec2ede4fe2ad52e267210c --- /dev/null +++ b/core/modules/media_entity/src/Plugin/views/wizard/MediaRevision.php @@ -0,0 +1,95 @@ + [ + 'value' => TRUE, + 'table' => 'media_field_revision', + 'field' => 'status', + 'plugin_id' => 'boolean', + 'entity_type' => 'media', + 'entity_field' => 'status', + ], + ]; + + /** + * {@inheritdoc} + */ + protected function defaultDisplayOptions() { + $display_options = parent::defaultDisplayOptions(); + + // Add permission-based access control. + $display_options['access']['type'] = 'perm'; + $display_options['access']['options']['perm'] = 'view all revisions'; + + // Remove the default fields, since we are customizing them here. + unset($display_options['fields']); + + /* Field: Media revision: Created date */ + $display_options['fields']['changed']['id'] = 'changed'; + $display_options['fields']['changed']['table'] = 'media_field_revision'; + $display_options['fields']['changed']['field'] = 'changed'; + $display_options['fields']['changed']['entity_type'] = 'media'; + $display_options['fields']['changed']['entity_field'] = 'changed'; + $display_options['fields']['changed']['alter']['alter_text'] = FALSE; + $display_options['fields']['changed']['alter']['make_link'] = FALSE; + $display_options['fields']['changed']['alter']['absolute'] = FALSE; + $display_options['fields']['changed']['alter']['trim'] = FALSE; + $display_options['fields']['changed']['alter']['word_boundary'] = FALSE; + $display_options['fields']['changed']['alter']['ellipsis'] = FALSE; + $display_options['fields']['changed']['alter']['strip_tags'] = FALSE; + $display_options['fields']['changed']['alter']['html'] = FALSE; + $display_options['fields']['changed']['hide_empty'] = FALSE; + $display_options['fields']['changed']['empty_zero'] = FALSE; + $display_options['fields']['changed']['plugin_id'] = 'field'; + $display_options['fields']['changed']['type'] = 'timestamp'; + $display_options['fields']['changed']['settings']['date_format'] = 'medium'; + $display_options['fields']['changed']['settings']['custom_date_format'] = ''; + $display_options['fields']['changed']['settings']['timezone'] = ''; + + /* Field: Media revision: Name */ + $display_options['fields']['name']['id'] = 'name'; + $display_options['fields']['name']['table'] = 'media_field_revision'; + $display_options['fields']['name']['field'] = 'name'; + $display_options['fields']['name']['entity_type'] = 'media'; + $display_options['fields']['name']['entity_field'] = 'name'; + $display_options['fields']['name']['label'] = ''; + $display_options['fields']['name']['alter']['alter_text'] = 0; + $display_options['fields']['name']['alter']['make_link'] = 0; + $display_options['fields']['name']['alter']['absolute'] = 0; + $display_options['fields']['name']['alter']['trim'] = 0; + $display_options['fields']['name']['alter']['word_boundary'] = 0; + $display_options['fields']['name']['alter']['ellipsis'] = 0; + $display_options['fields']['name']['alter']['strip_tags'] = 0; + $display_options['fields']['name']['alter']['html'] = 0; + $display_options['fields']['name']['hide_empty'] = 0; + $display_options['fields']['name']['empty_zero'] = 0; + $display_options['fields']['name']['settings']['link_to_entity'] = 0; + $display_options['fields']['name']['plugin_id'] = 'field'; + + return $display_options; + } + +} diff --git a/core/modules/media_entity/templates/media.html.twig b/core/modules/media_entity/templates/media.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..adbc176bfbe81775cd8130c157e07e30b85eb098 --- /dev/null +++ b/core/modules/media_entity/templates/media.html.twig @@ -0,0 +1,48 @@ +{# +/** + * @file + * Default theme implementation to present a media entity. + * + * Available variables: + * - media: The entity with limited access to object properties and methods. + * Only method names starting with "get", "has", or "is" and a few common + * methods such as "id", "label", and "bundle" are available. For example: + * - entity.getEntityTypeId() will return the entity type ID. + * - entity.hasField('field_example') returns TRUE if the entity includes + * field_example. (This does not indicate the presence of a value in this + * field.) + * Calling other methods, such as entity.delete(), will result in an exception. + * See \Drupal\Core\Entity\EntityInterface for a full list of methods. + * - name: Name of the media. + * - content: Media content. + * - title_prefix: Additional output populated by modules, intended to be + * displayed in front of the main title tag that appears in the template. + * - title_suffix: Additional output populated by modules, intended to be + * displayed after the main title tag that appears in the template. + * - view_mode: View mode; for example, "teaser" or "full". + * - attributes: HTML attributes for the containing element. + * - title_attributes: Same as attributes, except applied to the main title + * tag that appears in the template. + * + * @see template_preprocess_media() + * + * @ingroup themeable + */ +#} + + {# + In the 'full' view mode the entity label is assumed to be displayed as the + page title, so we do not display it here. + #} + {{ title_prefix }} + {% if label and view_mode != 'full' %} + + {{ label }} + + {% endif %} + {{ title_suffix }} + + {% if content %} + {{ content }} + {% endif %} + diff --git a/core/modules/media_entity/tests/modules/media_entity_test_bundle/config/install/media_entity.bundle.test.yml b/core/modules/media_entity/tests/modules/media_entity_test_bundle/config/install/media_entity.bundle.test.yml new file mode 100644 index 0000000000000000000000000000000000000000..c0a0f70480079cb0bdeaf7871c86d82d75f23748 --- /dev/null +++ b/core/modules/media_entity/tests/modules/media_entity_test_bundle/config/install/media_entity.bundle.test.yml @@ -0,0 +1,9 @@ +id: test +label: 'Test bundle' +description: 'Test bundle.' +type: generic +type_configuration: { } +status: true +langcode: en +dependencies: { } +field_map: { } diff --git a/core/modules/media_entity/tests/modules/media_entity_test_bundle/media_entity_test_bundle.info.yml b/core/modules/media_entity/tests/modules/media_entity_test_bundle/media_entity_test_bundle.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..9969dadcad8be8c067f36ff1d0f38b58f4ea7c6c --- /dev/null +++ b/core/modules/media_entity/tests/modules/media_entity_test_bundle/media_entity_test_bundle.info.yml @@ -0,0 +1,6 @@ +name: 'Media entity test bundle' +type: module +description: 'Provides test bundle for media entity.' +core: 8.x +package: Testing +version: VERSION diff --git a/core/modules/media_entity/tests/modules/media_entity_test_type/config/schema/media_entity_test_type.schema.yml b/core/modules/media_entity/tests/modules/media_entity_test_type/config/schema/media_entity_test_type.schema.yml new file mode 100644 index 0000000000000000000000000000000000000000..6ff7e145a2980a76d1e014b3acb75fd1cf8d87a7 --- /dev/null +++ b/core/modules/media_entity/tests/modules/media_entity_test_type/config/schema/media_entity_test_type.schema.yml @@ -0,0 +1,7 @@ +media_entity.bundle.type.test_type: + type: mapping + label: 'Test type configuration' + mapping: + test_config_value: + type: string + label: 'Test config value' diff --git a/core/modules/media_entity/tests/modules/media_entity_test_type/media_entity_test_type.info.yml b/core/modules/media_entity/tests/modules/media_entity_test_type/media_entity_test_type.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..ed82c4c71e35c78164fb29ae6374bd7bd8a3e521 --- /dev/null +++ b/core/modules/media_entity/tests/modules/media_entity_test_type/media_entity_test_type.info.yml @@ -0,0 +1,6 @@ +name: 'Test media type' +type: module +description: 'Provides test media type plugin to test configuration forms.' +core: 8.x +package: Testing +version: VERSION diff --git a/core/modules/media_entity/tests/modules/media_entity_test_type/src/Plugin/MediaEntity/Type/TestType.php b/core/modules/media_entity/tests/modules/media_entity_test_type/src/Plugin/MediaEntity/Type/TestType.php new file mode 100644 index 0000000000000000000000000000000000000000..60d68a06da4c6506fbc7ee671aa42861a8de19c6 --- /dev/null +++ b/core/modules/media_entity/tests/modules/media_entity_test_type/src/Plugin/MediaEntity/Type/TestType.php @@ -0,0 +1,51 @@ + $this->t('Field 1'), + 'field_2' => $this->t('Field 2'), + ]; + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return [ + 'test_config_value' => 'This is default value.', + ]; + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $form['test_config_value'] = [ + '#type' => 'textfield', + '#title' => $this->t('Test config value'), + '#default_value' => empty($this->configuration['test_config_value']) ? NULL : $this->configuration['test_config_value'], + ]; + + return $form; + } + +} diff --git a/core/modules/media_entity/tests/modules/media_entity_test_views/config/install/views.view.test_media_entity_bulk_form.yml b/core/modules/media_entity/tests/modules/media_entity_test_views/config/install/views.view.test_media_entity_bulk_form.yml new file mode 100644 index 0000000000000000000000000000000000000000..5c068b60ad5d3934149634efdb2a094d76965da7 --- /dev/null +++ b/core/modules/media_entity/tests/modules/media_entity_test_views/config/install/views.view.test_media_entity_bulk_form.yml @@ -0,0 +1,154 @@ +langcode: en +status: true +dependencies: + module: + - media_entity + - user +id: test_media_entity_bulk_form +label: '' +module: views +description: '' +tag: '' +base_table: media_field_data +base_field: mid +core: 8.x +display: + default: + display_plugin: default + id: default + display_title: Master + position: 0 + display_options: + style: + type: table + row: + type: fields + fields: + media_bulk_form: + id: media_bulk_form + table: media + field: media_bulk_form + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: false + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + action_title: 'With selection' + include_exclude: exclude + selected_actions: { } + entity_type: media + plugin_id: media_bulk_form + name: + id: name + table: media_field_data + field: name + entity_type: media + entity_field: media + hide_empty: false + empty_zero: false + settings: + link_to_entity: false + plugin_id: field + relationship: none + group_type: group + admin_label: '' + label: 'Media name' + exclude: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_alter_empty: true + click_sort_column: value + type: string + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + status: + id: status + table: media_field_data + field: status + relationship: none + group_type: group + admin_label: '' + label: Status + exclude: false + element_type: '' + element_class: '' + element_label_type: '' + element_label_class: '' + element_label_colon: true + element_wrapper_type: '' + element_wrapper_class: '' + element_default_classes: true + empty: '' + hide_empty: false + empty_zero: false + hide_alter_empty: true + click_sort_column: value + type: boolean + settings: + format: custom + format_custom_true: Published + format_custom_false: Unpublished + group_column: value + group_columns: { } + group_rows: true + delta_limit: 0 + delta_offset: 0 + delta_reversed: false + delta_first_last: false + multi_type: separator + separator: ', ' + field_api_classes: false + entity_type: media + entity_field: status + plugin_id: field + sorts: + mid: + id: mid + table: media_field_data + field: mid + relationship: none + group_type: group + admin_label: '' + order: ASC + exposed: false + expose: + label: '' + entity_type: media + entity_field: mid + plugin_id: standard + title: 'Entity bulk form test view' + header: { } + footer: { } + empty: { } + relationships: { } + arguments: { } + display_extenders: { } + page_1: + display_plugin: page + id: page_1 + display_title: Page + position: 1 + display_options: + path: test-media-entity-bulk-form diff --git a/core/modules/media_entity/tests/modules/media_entity_test_views/media_entity_test_views.info.yml b/core/modules/media_entity/tests/modules/media_entity_test_views/media_entity_test_views.info.yml new file mode 100644 index 0000000000000000000000000000000000000000..3c93c118e79cf9f27c7c902080a2b76792a3be57 --- /dev/null +++ b/core/modules/media_entity/tests/modules/media_entity_test_views/media_entity_test_views.info.yml @@ -0,0 +1,9 @@ +name: 'Media Entity test views' +type: module +description: 'Provides default views for views media entity tests.' +package: Testing +version: VERSION +core: 8.x +dependencies: + - media_entity + - views diff --git a/core/modules/media_entity/tests/src/Functional/MediaAccessTest.php b/core/modules/media_entity/tests/src/Functional/MediaAccessTest.php new file mode 100644 index 0000000000000000000000000000000000000000..b774fb599aa49a2f74d3238de7183ccb3c57ae6b --- /dev/null +++ b/core/modules/media_entity/tests/src/Functional/MediaAccessTest.php @@ -0,0 +1,102 @@ +testBundle = $this->drupalCreateMediaBundle(); + } + + /** + * Test some access control functionality. + */ + public function testMediaAccess() { + + $assert_session = $this->assertSession(); + + // Create media. + $media = Media::create([ + 'bundle' => $this->testBundle->id(), + 'name' => 'Unnamed', + ]); + $media->save(); + $user_media = Media::create([ + 'bundle' => $this->testBundle->id(), + 'name' => 'Unnamed', + 'uid' => $this->nonAdminUser->id(), + ]); + $user_media->save(); + + // We are logged-in as admin, so test 'administer media' permission. + $this->drupalGet('media/' . $user_media->id()); + $assert_session->statusCodeEquals(200); + $this->drupalGet('media/' . $user_media->id() . '/edit'); + $assert_session->statusCodeEquals(200); + $this->drupalGet('media/' . $user_media->id() . '/delete'); + $assert_session->statusCodeEquals(200); + + $this->drupalLogin($this->nonAdminUser); + /** @var \Drupal\user\RoleInterface $role */ + $role = Role::load('authenticated'); + + // Test 'view media' permission. + $this->drupalGet('media/' . $media->id()); + $assert_session->statusCodeEquals(403); + $this->grantPermissions($role, ['view media']); + $this->drupalGet('media/' . $media->id()); + $assert_session->statusCodeEquals(200); + + // Test 'create media' permission. + $this->drupalGet('media/add/' . $this->testBundle->id()); + $assert_session->statusCodeEquals(403); + $this->grantPermissions($role, ['create media']); + $this->drupalGet('media/add/' . $this->testBundle->id()); + $assert_session->statusCodeEquals(200); + + // Test 'update media' and 'delete media' permissions. + $this->drupalGet('media/' . $user_media->id() . '/edit'); + $assert_session->statusCodeEquals(403); + $this->drupalGet('media/' . $user_media->id() . '/delete'); + $assert_session->statusCodeEquals(403); + $this->grantPermissions($role, ['update media']); + $this->grantPermissions($role, ['delete media']); + $this->drupalGet('media/' . $user_media->id() . '/edit'); + $assert_session->statusCodeEquals(200); + $this->drupalGet('media/' . $user_media->id() . '/delete'); + $assert_session->statusCodeEquals(200); + + // Test 'update any media' and 'delete any media' permissions. + $this->drupalGet('media/' . $media->id() . '/edit'); + $assert_session->statusCodeEquals(403); + $this->drupalGet('media/' . $media->id() . '/delete'); + $assert_session->statusCodeEquals(403); + $this->grantPermissions($role, ['update any media']); + $this->grantPermissions($role, ['delete any media']); + $this->drupalGet('media/' . $media->id() . '/edit'); + $assert_session->statusCodeEquals(200); + $this->drupalGet('media/' . $media->id() . '/delete'); + $assert_session->statusCodeEquals(200); + + } + +} diff --git a/core/modules/media_entity/tests/src/Functional/MediaBulkFormTest.php b/core/modules/media_entity/tests/src/Functional/MediaBulkFormTest.php new file mode 100644 index 0000000000000000000000000000000000000000..5429766fbce8059b1d4118669bdbf8637cd76804 --- /dev/null +++ b/core/modules/media_entity/tests/src/Functional/MediaBulkFormTest.php @@ -0,0 +1,118 @@ +testBundle = $this->drupalCreateMediaBundle(); + + // Create some test media entities. + $this->mediaEntities = []; + for ($i = 1; $i <= 5; $i++) { + $media = Media::create([ + 'bundle' => $this->testBundle->id(), + ]); + $media->save(); + $this->mediaEntities[] = $media; + } + + } + + /** + * Tests the media bulk form. + */ + public function testBulkForm() { + + $session = $this->getSession(); + $page = $session->getPage(); + $assert_session = $this->assertSession(); + + // Check that all created entities are present in the test view. + $view = Views::getView('test_media_entity_bulk_form'); + $view->execute(); + $this->assertEquals($view->total_rows, 5); + + // Check the operations are accessible to the logged in user. + $this->drupalGet('test-media-entity-bulk-form'); + // Current available actions: Delete, Save, Publish, Unpublish. + $available_actions = [ + 'media_delete_action', + 'media_publish_action', + 'media_save_action', + 'media_unpublish_action', + ]; + foreach ($available_actions as $action_name) { + $assert_session->optionExists('action', $action_name); + } + + // Test unpublishing in bulk. + $page->checkField('media_bulk_form[0]'); + $page->checkField('media_bulk_form[1]'); + $page->checkField('media_bulk_form[2]'); + $page->selectFieldOption('action', 'media_unpublish_action'); + $page->pressButton('Apply to selected items'); + $assert_session->pageTextContains('Unpublish media was applied to 3 items'); + for ($i = 1; $i <= 3; $i++) { + $this->assertFalse($this->storage->loadUnchanged($i)->isPublished(), 'The unpublish action failed in some of the media entities.'); + } + + // Test publishing in bulk. + $page->checkField('media_bulk_form[0]'); + $page->checkField('media_bulk_form[1]'); + $page->selectFieldOption('action', 'media_publish_action'); + $page->pressButton('Apply to selected items'); + $assert_session->pageTextContains('Publish media was applied to 2 items'); + for ($i = 1; $i <= 2; $i++) { + $this->assertTrue($this->storage->loadUnchanged($i)->isPublished(), 'The publish action failed in some of the media entities.'); + } + + // Test deletion in bulk. + $page->checkField('media_bulk_form[0]'); + $page->checkField('media_bulk_form[1]'); + $page->selectFieldOption('action', 'media_delete_action'); + $page->pressButton('Apply to selected items'); + $assert_session->pageTextContains('Are you sure you want to delete these items?'); + $page->pressButton('Delete'); + $assert_session->pageTextContains('Deleted 2 media entities.'); + for ($i = 1; $i <= 2; $i++) { + $this->assertNull($this->storage->loadUnchanged($i), 'Could not delete some of the media entities.'); + } + + } + +} diff --git a/core/modules/media_entity/tests/src/Functional/MediaEntityFunctionalTestBase.php b/core/modules/media_entity/tests/src/Functional/MediaEntityFunctionalTestBase.php new file mode 100644 index 0000000000000000000000000000000000000000..5ddac2f250dc84180e2b2ce480ef677696ebfc6c --- /dev/null +++ b/core/modules/media_entity/tests/src/Functional/MediaEntityFunctionalTestBase.php @@ -0,0 +1,93 @@ +adminUser = $this->drupalCreateUser(static::$adminUserPermissions); + $this->nonAdminUser = $this->drupalCreateUser([]); + // Start off logged in as admin. + $this->drupalLogin($this->adminUser); + + $this->storage = $this->container->get('entity_type.manager')->getStorage('media'); + } + +} diff --git a/core/modules/media_entity/tests/src/Functional/MediaEntityFunctionalTestTrait.php b/core/modules/media_entity/tests/src/Functional/MediaEntityFunctionalTestTrait.php new file mode 100644 index 0000000000000000000000000000000000000000..4e2eedf05892758d86a7c733b3d14438e414b48d --- /dev/null +++ b/core/modules/media_entity/tests/src/Functional/MediaEntityFunctionalTestTrait.php @@ -0,0 +1,50 @@ +randomMachineName()); + } + else { + $id = $values['bundle']; + } + $values += [ + 'id' => $id, + 'label' => $id, + 'type' => $type_name, + 'type_configuration' => [], + 'field_map' => [], + 'new_revision' => FALSE, + ]; + + $bundle = MediaBundle::create($values); + $status = $bundle->save(); + + $this->assertEquals($status, SAVED_NEW, 'Could not create a media bundle of type ' . $type_name . '.'); + + return $bundle; + } + +} diff --git a/core/modules/media_entity/tests/src/Functional/MediaUiFunctionalTest.php b/core/modules/media_entity/tests/src/Functional/MediaUiFunctionalTest.php new file mode 100644 index 0000000000000000000000000000000000000000..930633455822f30cd3fb00b1d671362d9ec6212d --- /dev/null +++ b/core/modules/media_entity/tests/src/Functional/MediaUiFunctionalTest.php @@ -0,0 +1,166 @@ +drupalPlaceBlock('local_actions_block'); + $this->drupalPlaceBlock('local_tasks_block'); + } + + /** + * Tests the media actions (add/edit/delete). + */ + public function testMediaWithOnlyOneBundle() { + $session = $this->getSession(); + $page = $session->getPage(); + $assert_session = $this->assertSession(); + + /** @var \Drupal\media_entity\MediaBundleInterface $bundle */ + $bundle = $this->drupalCreateMediaBundle(['new_revision' => TRUE]); + + $this->drupalGet('media/add'); + $assert_session->statusCodeEquals(200); + $assert_session->addressEquals('media/add/' . $bundle->id()); + $assert_session->checkboxChecked('edit-revision'); + + // Tests media item add form. + $media_name = $this->randomMachineName(); + $page->fillField('name[0][value]', $media_name); + $revision_log_message = $this->randomString(); + $page->fillField('revision_log', $revision_log_message); + $page->pressButton('Save and publish'); + $media_id = $this->container->get('entity.query')->get('media')->execute(); + $media_id = reset($media_id); + /** @var \Drupal\media_entity\MediaInterface $media */ + $media = $this->container->get('entity_type.manager') + ->getStorage('media') + ->loadUnchanged($media_id); + $this->assertEquals($media->getRevisionLogMessage(), $revision_log_message); + $assert_session->titleEquals($media->label() . ' | Drupal'); + + // Tests media edit form. + $bundle->setNewRevision(FALSE); + $bundle->save(); + $media_name2 = $this->randomMachineName(); + $this->drupalGet('media/' . $media_id . '/edit'); + $assert_session->checkboxNotChecked('edit-revision'); + $media_name = $this->randomMachineName(); + $page->fillField('name[0][value]', $media_name2); + $page->pressButton('Save and keep published'); + $assert_session->titleEquals($media_name2 . ' | Drupal'); + + // Test that there is no empty vertical tabs element, if the container is + // empty (see #2750697). + // Make the "Publisher ID" and "Created" fields hidden. + $this->drupalGet('/admin/structure/media/manage/' . $bundle->id() . '/form-display'); + $page->selectFieldOption('fields[created][parent]', 'hidden'); + $page->selectFieldOption('fields[uid][parent]', 'hidden'); + $page->pressButton('Save'); + // Assure we are testing with a user without permission to manage revisions. + $this->drupalLogin($this->nonAdminUser); + // Check the container is not present. + $this->drupalGet('media/' . $media_id . '/edit'); + // An empty tab container would look like this. + $raw_html = '
' . "\n" . '
'; + $assert_session->responseNotContains($raw_html); + // Continue testing as admin. + $this->drupalLogin($this->adminUser); + + // Enable revisions by default. + $bundle->setNewRevision(TRUE); + $bundle->save(); + $this->drupalGet('media/' . $media_id . '/edit'); + $assert_session->checkboxChecked('edit-revision'); + $page->fillField('name[0][value]', $media_name); + $page->fillField('revision_log', $revision_log_message); + $page->pressButton('Save and keep published'); + $assert_session->titleEquals($media_name . ' | Drupal'); + /** @var \Drupal\media_entity\MediaInterface $media */ + $media = $this->container->get('entity_type.manager') + ->getStorage('media') + ->loadUnchanged($media_id); + $this->assertEquals($media->getRevisionLogMessage(), $revision_log_message); + + // Tests media delete form. + $this->drupalGet('media/' . $media_id . '/edit'); + $page->clickLink('Delete'); + $assert_session->pageTextContains('This action cannot be undone'); + $page->pressButton('Delete'); + $media_id = \Drupal::entityQuery('media')->execute(); + $this->assertFalse($media_id); + } + + /** + * Tests the "media/add" and "media/mid" pages. + * + * Tests if the "media/add" page gives you a selecting option if there are + * multiple media bundles available. + */ + public function testMediaWithMultipleBundles() { + $assert_session = $this->assertSession(); + + // Tests and creates the first media bundle. + $first_media_bundle = $this->drupalCreateMediaBundle(['description' => $this->randomMachineName(32)]); + + // Test and create a second media bundle. + $second_media_bundle = $this->drupalCreateMediaBundle(['description' => $this->randomMachineName(32)]); + + // Test if media/add displays two media bundle options. + $this->drupalGet('media/add'); + + // Checks for the first media bundle. + $assert_session->pageTextContains($first_media_bundle->label()); + $assert_session->pageTextContains($first_media_bundle->getDescription()); + // Checks for the second media bundle. + $assert_session->pageTextContains($second_media_bundle->label()); + $assert_session->pageTextContains($second_media_bundle->getDescription()); + + // Continue testing media bundle filter. + $first_media_item = Media::create(['bundle' => $first_media_bundle->id()]); + $first_media_item->save(); + $second_media_item = Media::create(['bundle' => $second_media_bundle->id()]); + $second_media_item->save(); + + // Go to first media item. + $this->drupalGet('media/' . $first_media_item->id()); + $assert_session->statusCodeEquals(200); + $assert_session->pageTextContains($first_media_item->label()); + + // Go to second media item. + $this->drupalGet('media/' . $second_media_item->id()); + $assert_session->statusCodeEquals(200); + $assert_session->pageTextContains($second_media_item->label()); + + } + +} diff --git a/core/modules/media_entity/tests/src/FunctionalJavascript/DocumentTest.php b/core/modules/media_entity/tests/src/FunctionalJavascript/DocumentTest.php new file mode 100644 index 0000000000000000000000000000000000000000..b02ad231c878508fa779f13df6f09cabdf1eb27b --- /dev/null +++ b/core/modules/media_entity/tests/src/FunctionalJavascript/DocumentTest.php @@ -0,0 +1,57 @@ +getSession(); + $page = $session->getPage(); + $assert_session = $this->assertSession(); + + $bundle_name = strtolower($this->randomMachineName(12)); + $provided_fields = ['mime', 'size']; + $this->createMediaBundleTest($bundle_name, 'document', $provided_fields); + // Create a supported and a non-supported field. + $fields = [ + 'field_string1' => 'string', + 'field_file1' => 'file', + ]; + $this->createMediaFields($fields, $bundle_name); + // Adjust the allowed extensions on the file field. + $file_field = FieldConfig::load("media.$bundle_name.field_file1"); + $file_field->setSetting('file_extensions', 'txt')->save(); + $this->drupalGet("admin/structure/media/manage/$bundle_name"); + $this->assertSelectOptions("type_configuration[document][source_field]", ['field_file1'], ['field_string1']); + $page->selectFieldOption("type_configuration[document][source_field]", 'field_file1'); + $page->pressButton('Save media bundle'); + $this->drupalGet('admin/structure/media'); + // Hide the media name to test default name generation. + $this->hideMediaField('name', $bundle_name); + // Create a media item. + $this->drupalGet("media/add/$bundle_name"); + $page->attachFileToField("files[field_file1_0]", \Drupal::root() . '/sites/README.txt'); + $assert_session->assertWaitOnAjaxRequest(); + $page->pressButton('Save and publish'); + + $assert_session->addressEquals('media/1'); + // Make sure the thumbnail shows up. + $assert_session->elementAttributeContains('css', '.image-style-thumbnail', 'src', 'generic.png'); + // Load the media and check its default name. + $media = Media::load(1); + $this->assertEquals($media->label(), 'README.txt'); + + } + +} diff --git a/core/modules/media_entity/tests/src/FunctionalJavascript/MediaEntityJavascriptTestBase.php b/core/modules/media_entity/tests/src/FunctionalJavascript/MediaEntityJavascriptTestBase.php new file mode 100644 index 0000000000000000000000000000000000000000..4f214f344086fc8431dbd5b4fd175c94573d4901 --- /dev/null +++ b/core/modules/media_entity/tests/src/FunctionalJavascript/MediaEntityJavascriptTestBase.php @@ -0,0 +1,109 @@ +adminUser = $this->drupalCreateUser(static::$adminUserPermissions); + $this->nonAdminUser = $this->drupalCreateUser([]); + // Start off logged in as admin. + $this->drupalLogin($this->adminUser); + + $this->storage = $this->container->get('entity_type.manager')->getStorage('media'); + } + + /** + * Waits and asserts that a given element is visible. + * + * @param string $selector + * The CSS selector. + * @param int $timeout + * (Optional) Timeout in milliseconds, defaults to 1000. + * @param string $message + * (Optional) Message to pass to assertJsCondition(). + */ + protected function waitUntilVisible($selector, $timeout = 1000, $message = '') { + $condition = "jQuery('" . $selector . ":visible').length > 0"; + $this->assertJsCondition($condition, $timeout, $message); + } + +} diff --git a/core/modules/media_entity/tests/src/FunctionalJavascript/MediaTypeTestBase.php b/core/modules/media_entity/tests/src/FunctionalJavascript/MediaTypeTestBase.php new file mode 100644 index 0000000000000000000000000000000000000000..0b00862f27d9b207c3ba4797bd68daa0750fa7dd --- /dev/null +++ b/core/modules/media_entity/tests/src/FunctionalJavascript/MediaTypeTestBase.php @@ -0,0 +1,143 @@ + $field_name, + 'entity_type' => 'media', + 'type' => $field_type, + ]); + $storage->save(); + $instance = FieldConfig::create([ + 'field_storage' => $storage, + 'bundle' => $bundle_name, + ]); + $instance->save(); + // Make the field visible in the form display. + /** @var \Drupal\Core\Entity\Display\EntityFormDisplayInterface $form_display */ + $component = \Drupal::service('plugin.manager.field.widget') + ->prepareConfiguration($field_type, []); + entity_get_form_display('media', $bundle_name, 'default') + ->setComponent($field_name, $component) + ->save(); + } + + /** + * Helper to create a set of fields in a bundle. + * + * @param array $fields + * An associative array where keys are field names and values field types. + * @param string $bundle_name + * The bundle machine name. + */ + protected function createMediaFields(array $fields, $bundle_name) { + foreach ($fields as $field_name => $field_type) { + $this->createMediaField($field_name, $field_type, $bundle_name); + } + } + + /** + * Hide a component from the default form display config. + * + * @param string $field_name + * The field name. + * @param string $bundle_name + * The media bundle machine name. + */ + protected function hideMediaField($field_name, $bundle_name) { + $form_display = entity_get_form_display('media', $bundle_name, 'default'); + $form_display->removeComponent($field_name)->save(); + } + + /** + * Helper to test a generic bundle creation. + * + * @param string $bundle_name + * The bundle machine name. + * @param string $bundle_type + * The bundle type ID. + * @param array $provided_fields + * (optional) An array of field machine names this type provides. + * @param bool $check_source_field_element + * (optional) Whether this type provides a "source_field". Defaults to TRUE. + */ + public function createMediaBundleTest($bundle_name, $bundle_type, array $provided_fields = [], $check_source_field_element = TRUE) { + $session = $this->getSession(); + $page = $session->getPage(); + $assert_session = $this->assertSession(); + + $this->drupalGet('admin/structure/media/add'); + $page->fillField('label', $bundle_name); + // assertWaitOnAjaxRequest() doesn't work on the machine name element. + $session->wait(5000, "jQuery('.machine-name-value').text() === '$bundle_name'"); + + // Make sure the bundle type is available as plugin type. + $assert_session->optionExists('type', $bundle_type); + $page->selectFieldOption('type', $bundle_type); + $assert_session->assertWaitOnAjaxRequest(); + + // Select our plugin type, and make sure the form gets updated. + if ($check_source_field_element) { + $assert_session->selectExists("type_configuration[$bundle_type][source_field]"); + } + + // Make sure the provided fields are visible on the form. + if (!empty($provided_fields)) { + foreach ($provided_fields as $provided_field) { + $assert_session->selectExists("field_mapping[$provided_field]"); + } + } + + // Save the page to create the bundle. + $page->pressButton('Save media bundle'); + $assert_session->pageTextContains('The media bundle ' . $bundle_name . ' has been added.'); + $this->drupalGet('admin/structure/media'); + $assert_session->pageTextContains($bundle_name); + + } + + /** + * Helper to assert presence/absence of select options. + * + * @param string $select + * One of id|name|label|value for the select field. + * @param array $expected_options + * An indexed array of expected option names. + * @param array $non_expected_options + * An indexed array of non-expected option names. + * + * @see \Drupal\Tests\WebAssert::optionExists() + * @see \Drupal\Tests\WebAssert::optionNotExists() + */ + protected function assertSelectOptions($select, $expected_options = [], $non_expected_options = []) { + $assert_session = $this->assertSession(); + foreach ($expected_options as $expected_option) { + $assert_session->optionExists($select, $expected_option); + } + foreach ($non_expected_options as $non_expected_option) { + $assert_session->optionNotExists($select, $non_expected_option); + } + } + +} diff --git a/core/modules/media_entity/tests/src/FunctionalJavascript/MediaUiJavascriptTest.php b/core/modules/media_entity/tests/src/FunctionalJavascript/MediaUiJavascriptTest.php new file mode 100644 index 0000000000000000000000000000000000000000..671118122002ebe4a00bf17b39d53b5f6b25e474 --- /dev/null +++ b/core/modules/media_entity/tests/src/FunctionalJavascript/MediaUiJavascriptTest.php @@ -0,0 +1,169 @@ +drupalPlaceBlock('local_actions_block'); + $this->drupalPlaceBlock('local_tasks_block'); + } + + /** + * Tests a media bundle administration. + */ + public function testMediaBundles() { + $session = $this->getSession(); + $page = $session->getPage(); + $assert_session = $this->assertSession(); + + // Test the creation of a media bundle using the UI. + $name = $this->randomMachineName(); + $description = $this->randomMachineName(); + $this->drupalGet('admin/structure/media/add'); + $page->fillField('label', $name); + $session->wait(2000); + $page->selectFieldOption('type', 'generic'); + $page->fillField('description', $description); + $page->pressButton('Save media bundle'); + $assert_session->statusCodeEquals(200); + $assert_session->pageTextContains('The media bundle ' . $name . ' has been added.'); + $this->drupalGet('admin/structure/media'); + $assert_session->statusCodeEquals(200); + $assert_session->pageTextContains($name); + $assert_session->pageTextContains($description); + + /** @var \Drupal\media_entity\MediaBundleInterface $bundle_storage */ + $bundle_storage = $this->container->get('entity_type.manager')->getStorage('media_bundle'); + $this->testBundle = $bundle_storage->load(strtolower($name)); + + // Check if all action links exist. + $assert_session->linkByHrefExists('admin/structure/media/add'); + $assert_session->linkByHrefExists('admin/structure/media/manage/' . $this->testBundle->id()); + $assert_session->linkByHrefExists('admin/structure/media/manage/' . $this->testBundle->id() . '/fields'); + $assert_session->linkByHrefExists('admin/structure/media/manage/' . $this->testBundle->id() . '/form-display'); + $assert_session->linkByHrefExists('admin/structure/media/manage/' . $this->testBundle->id() . '/display'); + + // Assert that fields have expected values before editing. + $page->clickLink('Edit'); + $assert_session->fieldValueEquals('label', $name); + $assert_session->fieldValueEquals('description', $description); + $assert_session->fieldValueEquals('type', 'generic'); + $assert_session->fieldValueEquals('label', $name); + $assert_session->checkboxNotChecked('edit-options-new-revision'); + $assert_session->checkboxChecked('edit-options-status'); + $assert_session->checkboxNotChecked('edit-options-queue-thumbnail-downloads'); + $assert_session->pageTextContains('Create new revision'); + $assert_session->pageTextContains('Automatically create a new revision of media entities. Users with the Administer media permission will be able to override this option.'); + $assert_session->pageTextContains('Download thumbnails via a queue.'); + $assert_session->pageTextContains('Entities will be automatically published when they are created.'); + $assert_session->pageTextContains('No metadata fields available.'); + $assert_session->pageTextContains('Media type plugins can provide metadata fields such as title, caption, size information, credits, ... Media entity can automatically save this metadata information to entity fields, which can be configured below. Information will only be mapped if the entity field is empty.'); + + // Try to change media type and check if new configuration sub-form appears. + $page->selectFieldOption('type', 'test_type'); + $assert_session->assertWaitOnAjaxRequest(); + $assert_session->fieldExists('Test config value'); + $assert_session->fieldValueEquals('Test config value', 'This is default value.'); + $assert_session->fieldExists('Field 1'); + $assert_session->fieldExists('Field 2'); + + // Test if the edit machine name is not editable. + $assert_session->fieldDisabled('Machine-readable name'); + + // Edit and save media bundle form fields with new values. + $new_name = $this->randomMachineName(); + $new_description = $this->randomMachineName(); + $page->fillField('label', $new_name); + $page->fillField('description', $new_description); + $page->selectFieldOption('type', 'test_type'); + $page->fillField('Test config value', 'This is new config value.'); + $page->selectFieldOption('field_mapping[field_1]', 'name'); + $page->checkField('options[new_revision]'); + $page->uncheckField('options[status]'); + $page->checkField('options[queue_thumbnail_downloads]'); + $page->pressButton('Save media bundle'); + $assert_session->statusCodeEquals(200); + + // Test if edit worked and if new field values have been saved as expected. + $this->drupalGet('admin/structure/media/manage/' . $this->testBundle->id()); + $assert_session->fieldValueEquals('label', $new_name); + $assert_session->fieldValueEquals('description', $new_description); + $assert_session->fieldValueEquals('type', 'test_type'); + $assert_session->checkboxChecked('options[new_revision]'); + $assert_session->checkboxNotChecked('options[status]'); + $assert_session->checkboxChecked('options[queue_thumbnail_downloads]'); + $assert_session->fieldValueEquals('Test config value', 'This is new config value.'); + $assert_session->fieldValueEquals('Field 1', 'name'); + $assert_session->fieldValueEquals('Field 2', '_none'); + + /** @var \Drupal\media_entity\MediaBundleInterface $loaded_bundle */ + $loaded_bundle = $this->container->get('entity_type.manager') + ->getStorage('media_bundle') + ->load($this->testBundle->id()); + $this->assertEquals($loaded_bundle->id(), $this->testBundle->id()); + $this->assertEquals($loaded_bundle->label(), $new_name); + $this->assertEquals($loaded_bundle->getDescription(), $new_description); + $this->assertEquals($loaded_bundle->getType()->getPluginId(), 'test_type'); + $this->assertEquals($loaded_bundle->getType()->getConfiguration()['test_config_value'], 'This is new config value.'); + $this->assertTrue($loaded_bundle->shouldCreateNewRevision()); + $this->assertTrue($loaded_bundle->getQueueThumbnailDownloads()); + $this->assertFalse($loaded_bundle->getStatus()); + $this->assertEquals($loaded_bundle->getFieldMap(), ['field_1' => 'name']); + + // Test that a media being created with default status to "FALSE" will be + // created unpublished. + /** @var \Drupal\media_entity\MediaInterface $unpublished_media */ + $unpublished_media = Media::create(['name' => 'unpublished test media', 'bundle' => $loaded_bundle->id()]); + $this->assertFalse($unpublished_media->isPublished()); + $unpublished_media->delete(); + + // Tests media bundle delete form. + $page->clickLink('Delete'); + $assert_session->addressEquals('admin/structure/media/manage/' . $this->testBundle->id() . '/delete'); + $page->pressButton('Delete'); + $assert_session->addressEquals('admin/structure/media'); + $assert_session->pageTextContains('The media bundle ' . $new_name . ' has been deleted.'); + + // Test bundle delete prevention when there is existing media. + $bundle2 = $this->drupalCreateMediaBundle(); + $label2 = $bundle2->label(); + $media = Media::create(['name' => 'lorem ipsum', 'bundle' => $bundle2->id()]); + $media->save(); + $this->drupalGet('admin/structure/media/manage/' . $bundle2->id()); + $page->clickLink('Delete'); + $assert_session->addressEquals('admin/structure/media/manage/' . $bundle2->id() . '/delete'); + $assert_session->fieldNotExists('edit-submit'); + $assert_session->pageTextContains("$label2 is used by 1 piece of content on your site. You can not remove this content type until you have removed all of the $label2 content."); + + } + +} diff --git a/core/modules/media_entity/tests/src/FunctionalJavascript/MediaViewsWizardTest.php b/core/modules/media_entity/tests/src/FunctionalJavascript/MediaViewsWizardTest.php new file mode 100644 index 0000000000000000000000000000000000000000..ac734cfe0e93e265ac80ee4fc11f347e97bcaa8c --- /dev/null +++ b/core/modules/media_entity/tests/src/FunctionalJavascript/MediaViewsWizardTest.php @@ -0,0 +1,89 @@ +getSession(); + $page = $session->getPage(); + $assert_session = $this->assertSession(); + + $view_id = strtolower($this->randomMachineName(16)); + $this->drupalGet('admin/structure/views/add'); + $page->fillField('label', $view_id); + $this->waitUntilVisible('.machine-name-value'); + $page->selectFieldOption('show[wizard_key]', 'media'); + $assert_session->assertWaitOnAjaxRequest(); + $page->checkField('page[create]'); + $page->fillField('page[path]', $this->randomMachineName(16)); + $page->pressButton('Save and edit'); + $assert_session->assertWaitOnAjaxRequest(); + $this->assertEquals($session->getCurrentUrl(), $this->baseUrl . '/admin/structure/views/view/' . $view_id); + + $view = Views::getView($view_id); + $view->initHandlers(); + $row = $view->display_handler->getOption('row'); + $this->assertEquals($row['type'], 'fields'); + // Check for the default filters. + $this->assertEquals($view->filter['status']->table, 'media_field_data'); + $this->assertEquals($view->filter['status']->field, 'status'); + $this->assertTrue($view->filter['status']->value); + // Check for the default fields. + $this->assertEquals($view->field['name']->table, 'media_field_data'); + $this->assertEquals($view->field['name']->field, 'name'); + + } + + /** + * Tests adding a view of media revisions. + */ + public function testMediaRevisionWizard() { + $session = $this->getSession(); + $page = $session->getPage(); + $assert_session = $this->assertSession(); + + $view_id = strtolower($this->randomMachineName(16)); + $this->drupalGet('admin/structure/views/add'); + $page->fillField('label', $view_id); + $this->waitUntilVisible('.machine-name-value'); + $page->selectFieldOption('show[wizard_key]', 'media_revision'); + $assert_session->assertWaitOnAjaxRequest(); + $page->checkField('page[create]'); + $page->fillField('page[path]', $this->randomMachineName(16)); + $page->pressButton('Save and edit'); + $assert_session->assertWaitOnAjaxRequest(); + $this->assertEquals($session->getCurrentUrl(), $this->baseUrl . '/admin/structure/views/view/' . $view_id); + + $view = Views::getView($view_id); + $view->initHandlers(); + $row = $view->display_handler->getOption('row'); + $this->assertEquals($row['type'], 'fields'); + + // Check for the default filters. + $this->assertEquals($view->filter['status']->table, 'media_field_revision'); + $this->assertEquals($view->filter['status']->field, 'status'); + $this->assertTrue($view->filter['status']->value); + + // Check for the default fields. + $this->assertEquals($view->field['name']->table, 'media_field_revision'); + $this->assertEquals($view->field['name']->field, 'name'); + $this->assertEquals($view->field['changed']->table, 'media_field_revision'); + $this->assertEquals($view->field['changed']->field, 'changed'); + + } + +} diff --git a/core/modules/media_entity/tests/src/Kernel/BasicCreationTest.php b/core/modules/media_entity/tests/src/Kernel/BasicCreationTest.php new file mode 100644 index 0000000000000000000000000000000000000000..d5aef442eff698e5b2c040e12d07951a8641c260 --- /dev/null +++ b/core/modules/media_entity/tests/src/Kernel/BasicCreationTest.php @@ -0,0 +1,114 @@ +installEntitySchema('user'); + $this->installEntitySchema('file'); + $this->installSchema('file', 'file_usage'); + $this->installEntitySchema('media'); + $this->installConfig(['field', 'system', 'image', 'file']); + + // Create a test bundle. + $id = strtolower($this->randomMachineName()); + $this->testBundle = MediaBundle::create([ + 'id' => $id, + 'label' => $id, + 'type' => 'generic', + 'type_configuration' => [], + 'field_map' => [], + 'new_revision' => FALSE, + ]); + $this->testBundle->save(); + + } + + /** + * Tests creating a media bundle programmatically. + */ + public function testMediaBundleCreation() { + /** @var \Drupal\media_entity\MediaBundleInterface $bundle_storage */ + $bundle_storage = $this->container->get('entity_type.manager')->getStorage('media_bundle'); + + $bundle_exists = (bool) $bundle_storage->load($this->testBundle->id()); + $this->assertTrue($bundle_exists, 'The new media bundle has not been correctly created in the database.'); + + // Test default bundle created from default configuration. + $this->container->get('module_installer')->install(['media_entity_test_bundle']); + $test_bundle = $bundle_storage->load('test'); + $this->assertTrue((bool) $test_bundle, 'The media bundle from default configuration has not been created in the database.'); + $this->assertEquals($test_bundle->get('label'), 'Test bundle', 'Could not assure the correct bundle label.'); + $this->assertEquals($test_bundle->get('description'), 'Test bundle.', 'Could not assure the correct bundle description.'); + $this->assertEquals($test_bundle->get('type'), 'generic', 'Could not assure the correct bundle plugin type.'); + $this->assertEquals($test_bundle->get('type_configuration'), [], 'Could not assure the correct plugin configuration.'); + $this->assertEquals($test_bundle->get('field_map'), [], 'Could not assure the correct field map.'); + } + + /** + * Tests creating a media entity programmatically. + */ + public function testMediaEntityCreation() { + $media = Media::create([ + 'bundle' => $this->testBundle->id(), + 'name' => 'Unnamed', + ]); + $media->save(); + + $media_not_exist = (bool) Media::load(rand(1000, 9999)); + $this->assertFalse($media_not_exist, 'Failed asserting a non-existent media.'); + + $media_exists = (bool) Media::load($media->id()); + $this->assertTrue($media_exists, 'The new media entity has not been created in the database.'); + $this->assertEquals($media->bundle(), $this->testBundle->id(), 'The media was not created with the correct bundle.'); + $this->assertEquals($media->label(), 'Unnamed', 'The media was not created with the correct name.'); + + // Test the creation of a media without user-defined label and check if a + // default name is provided. + $media = Media::create([ + 'bundle' => $this->testBundle->id(), + ]); + $media->save(); + $expected_name = 'media' . ':' . $this->testBundle->id() . ':' . $media->uuid(); + $this->assertEquals($media->bundle(), $this->testBundle->id(), 'The media was not created with correct bundle.'); + $this->assertEquals($media->label(), $expected_name, 'The media was not created with a default name.'); + } + +} diff --git a/core/modules/media_entity/tests/src/Kernel/TokensTest.php b/core/modules/media_entity/tests/src/Kernel/TokensTest.php new file mode 100644 index 0000000000000000000000000000000000000000..8d7032aadd6193b1581df47fdae2467919923893 --- /dev/null +++ b/core/modules/media_entity/tests/src/Kernel/TokensTest.php @@ -0,0 +1,76 @@ +installEntitySchema('file'); + $this->installSchema('file', 'file_usage'); + $this->installEntitySchema('media'); + $this->installConfig(['language', 'datetime', 'field', 'system']); + } + + /** + * Tests some of the tokens provided by media_entity. + */ + public function testMediaEntityTokens() { + // Create a generic media bundle. + $bundle_name = $this->randomMachineName(); + + MediaBundle::create([ + 'id' => $bundle_name, + 'label' => $bundle_name, + 'type' => 'generic', + 'type_configuration' => [], + 'field_map' => [], + 'status' => 1, + 'new_revision' => FALSE, + ])->save(); + + // Create a media entity. + $media = Media::create([ + 'name' => $this->randomMachineName(), + 'bundle' => $bundle_name, + 'uid' => '1', + 'langcode' => Language::LANGCODE_DEFAULT, + 'status' => TRUE, + ]); + $media->save(); + + $token_service = $this->container->get('token'); + + $replaced_value = $token_service->replace('[media:name]', ['media' => $media]); + $this->assertEquals($media->label(), $replaced_value, 'Token replacement for the media label was sucessful.'); + + } + +} diff --git a/core/profiles/standard/config/optional/media_entity.bundle.document.yml b/core/profiles/standard/config/optional/media_entity.bundle.document.yml new file mode 100644 index 0000000000000000000000000000000000000000..0812e8ab82e1ddd8ffc7b1917256fee8c93962ef --- /dev/null +++ b/core/profiles/standard/config/optional/media_entity.bundle.document.yml @@ -0,0 +1,14 @@ +langcode: en +status: true +dependencies: + module: + - media_entity +id: document +label: Document +description: 'Use Document for uploading document files.' +type: document +queue_thumbnail_downloads: false +new_revision: false +type_configuration: + source_field: field_media_source_document +field_map: { } diff --git a/core/themes/classy/templates/content/media.html.twig b/core/themes/classy/templates/content/media.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..2bdd59e9604799b8b73372000217390d1143cebc --- /dev/null +++ b/core/themes/classy/templates/content/media.html.twig @@ -0,0 +1,19 @@ +{# +/** + * @file + * Theme override to display a media. + * + * Available variables: + * - name: Name of the media. + * - content: Media content. + * + * @see template_preprocess_media() + * + * @ingroup themeable + */ +#} + + {% if content %} + {{ content }} + {% endif %} + diff --git a/core/themes/stable/templates/content/media.html.twig b/core/themes/stable/templates/content/media.html.twig new file mode 100644 index 0000000000000000000000000000000000000000..2bdd59e9604799b8b73372000217390d1143cebc --- /dev/null +++ b/core/themes/stable/templates/content/media.html.twig @@ -0,0 +1,19 @@ +{# +/** + * @file + * Theme override to display a media. + * + * Available variables: + * - name: Name of the media. + * - content: Media content. + * + * @see template_preprocess_media() + * + * @ingroup themeable + */ +#} + + {% if content %} + {{ content }} + {% endif %} +