diff --git a/core/composer.json b/core/composer.json index 71f07d0..8f69344 100644 --- a/core/composer.json +++ b/core/composer.json @@ -109,6 +109,7 @@ "drupal/link": "self.version", "drupal/locale": "self.version", "drupal/minimal": "self.version", + "drupal/media": "self.version", "drupal/menu_link_content": "self.version", "drupal/menu_ui": "self.version", "drupal/migrate": "self.version", diff --git a/core/modules/media/config/install/media.settings.yml b/core/modules/media/config/install/media.settings.yml new file mode 100644 index 0000000..85f1c81 --- /dev/null +++ b/core/modules/media/config/install/media.settings.yml @@ -0,0 +1 @@ +icon_base: 'public://media-icons/generic' diff --git a/core/modules/media/config/optional/system.action.media_delete_action.yml b/core/modules/media/config/optional/system.action.media_delete_action.yml new file mode 100644 index 0000000..62af35e --- /dev/null +++ b/core/modules/media/config/optional/system.action.media_delete_action.yml @@ -0,0 +1,10 @@ +langcode: en +status: true +dependencies: + module: + - media +id: media_delete_action +label: 'Delete media' +type: media +plugin: media_delete_action +configuration: { } diff --git a/core/modules/media/config/optional/system.action.media_publish_action.yml b/core/modules/media/config/optional/system.action.media_publish_action.yml new file mode 100644 index 0000000..95e173d --- /dev/null +++ b/core/modules/media/config/optional/system.action.media_publish_action.yml @@ -0,0 +1,10 @@ +langcode: en +status: true +dependencies: + module: + - media +id: media_publish_action +label: 'Publish media' +type: media +plugin: media_publish_action +configuration: { } diff --git a/core/modules/media/config/optional/system.action.media_save_action.yml b/core/modules/media/config/optional/system.action.media_save_action.yml new file mode 100644 index 0000000..c4d098f --- /dev/null +++ b/core/modules/media/config/optional/system.action.media_save_action.yml @@ -0,0 +1,10 @@ +langcode: en +status: true +dependencies: + module: + - media +id: media_save_action +label: 'Save media' +type: media +plugin: media_save_action +configuration: { } diff --git a/core/modules/media/config/optional/system.action.media_unpublish_action.yml b/core/modules/media/config/optional/system.action.media_unpublish_action.yml new file mode 100644 index 0000000..4189e4e --- /dev/null +++ b/core/modules/media/config/optional/system.action.media_unpublish_action.yml @@ -0,0 +1,10 @@ +langcode: en +status: true +dependencies: + module: + - media +id: media_unpublish_action +label: 'Unpublish media' +type: media +plugin: media_unpublish_action +configuration: { } diff --git a/core/modules/media/config/schema/media.schema.yml b/core/modules/media/config/schema/media.schema.yml new file mode 100644 index 0000000..b326f9b --- /dev/null +++ b/core/modules/media/config/schema/media.schema.yml @@ -0,0 +1,72 @@ +media.settings: + type: config_object + label: 'Media settings' + mapping: + icon_base: + type: string + label: 'URI where media icons will be installed' + +media.type.*: + type: config_entity + label: 'Media type' + mapping: + id: + type: string + label: 'Machine name' + label: + type: label + label: 'Name' + description: + type: text + label: 'Description' + handler: + type: string + label: 'Handler' + handler_configuration: + type: media.handler.[%parent.handler] + queue_thumbnail_downloads: + type: boolean + label: 'Queue thumbnail downloads' + new_revision: + type: boolean + label: 'Whether a new revision should be created by default' + field_map: + type: sequence + label: 'Field map' + sequence: + type: string + +media.handler.file: + type: media.handler.field_aware + label: 'File handler configuration' + +action.configuration.media_delete_action: + type: action_configuration_default + label: 'Delete media configuration' + +action.configuration.media_save_action: + type: action_configuration_default + label: 'Save media configuration' + +action.configuration.media_publish_action: + type: action_configuration_default + label: 'Publish media configuration' + +action.configuration.media_unpublish_action: + type: action_configuration_default + label: 'Unpublish media configuration' + +field.formatter.settings.media_thumbnail: + type: field.formatter.settings.image + label: 'Media thumbnail field display format settings' + +media.handler.*: + type: mapping + label: 'Media handler plugin settings' + +media.handler.field_aware: + type: mapping + mapping: + source_field: + type: string + label: 'Source field' diff --git a/core/modules/media/config/schema/media.views.schema.yml b/core/modules/media/config/schema/media.views.schema.yml new file mode 100644 index 0000000..0c7371d --- /dev/null +++ b/core/modules/media/config/schema/media.views.schema.yml @@ -0,0 +1,5 @@ +# Schema for the views plugins of the Media module. + +views.field.media_bulk_form: + type: views_field_bulk_form + label: 'Media bulk form' diff --git a/core/modules/media/images/icons/generic.png b/core/modules/media/images/icons/generic.png new file mode 100644 index 0000000..46125e7 --- /dev/null +++ b/core/modules/media/images/icons/generic.png @@ -0,0 +1,8 @@ +PNG + + IHDRetEXtSoftwareAdobe ImageReadyqe<IDATxK*[Q#ʄ2(8T(.*?>񦏋"$>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/js/media_form.js b/core/modules/media/js/media_form.js new file mode 100644 index 0000000..f9c2d11 --- /dev/null +++ b/core/modules/media/js/media_form.js @@ -0,0 +1,40 @@ +/** + * @file + * Defines Javascript behaviors for the media entity form. + */ + +(function ($, Drupal) { + + '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-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); diff --git a/core/modules/media/js/media_type_form.js b/core/modules/media/js/media_type_form.js new file mode 100644 index 0000000..48945ea --- /dev/null +++ b/core/modules/media/js/media_type_form.js @@ -0,0 +1,46 @@ +/** + * @file + * Javascript for the media type form. + */ + +(function ($, Drupal) { + + 'use strict'; + + /** + * Behaviors for setting summaries on media type form. + * + * @type {Drupal~behavior} + * + * @prop {Drupal~behaviorAttach} attach + * Attaches summary behaviors on media type 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/media.api.php b/core/modules/media/media.api.php new file mode 100644 index 0000000..bd5df52 --- /dev/null +++ b/core/modules/media/media.api.php @@ -0,0 +1,25 @@ +get('icon_base'); + if (!file_prepare_directory($destination, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) { + throw new \RuntimeException("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 \RuntimeException("Unable to copy {$file->uri} to $destination."); + } + } +} diff --git a/core/modules/media/media.libraries.yml b/core/modules/media/media.libraries.yml new file mode 100644 index 0000000..ef286b1 --- /dev/null +++ b/core/modules/media/media.libraries.yml @@ -0,0 +1,13 @@ +media_form: + version: VERSION + js: + 'js/media_form.js': {} + dependencies: + - core/drupal.form + +media_type_form: + version: VERSION + js: + 'js/media_type_form.js': {} + dependencies: + - core/drupal.form diff --git a/core/modules/media/media.links.action.yml b/core/modules/media/media.links.action.yml new file mode 100644 index 0000000..99ddcd8 --- /dev/null +++ b/core/modules/media/media.links.action.yml @@ -0,0 +1,10 @@ +media.bundle_add: + route_name: entity.media_type.add_form + title: 'Add media type' + appears_on: + - entity.media_type.collection + +media.add: + route_name: entity.media.add_page + title: 'Add media' + weight: 10 diff --git a/core/modules/media/media.links.contextual.yml b/core/modules/media/media.links.contextual.yml new file mode 100644 index 0000000..1945ef5 --- /dev/null +++ b/core/modules/media/media.links.contextual.yml @@ -0,0 +1,10 @@ +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/media.links.menu.yml b/core/modules/media/media.links.menu.yml new file mode 100644 index 0000000..60c5bff --- /dev/null +++ b/core/modules/media/media.links.menu.yml @@ -0,0 +1,11 @@ +entity.media_type.collection: + title: 'Media types' + parent: system.admin_structure + description: 'Manage media types.' + route_name: entity.media_type.collection + +entity.media.add: + title: 'Add media' + parent: entity.media.collection + description: 'Add a new media entity.' + route_name: entity.media.add_page diff --git a/core/modules/media/media.links.task.yml b/core/modules/media/media.links.task.yml new file mode 100644 index 0000000..af33e94 --- /dev/null +++ b/core/modules/media/media.links.task.yml @@ -0,0 +1,25 @@ +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_type.edit_form: + title: 'Edit' + route_name: entity.media_type.edit_form + base_route: entity.media_type.edit_form + +entity.media_type.collection: + title: List + route_name: entity.media_type.collection + base_route: entity.media_type.collection diff --git a/core/modules/media/media.module b/core/modules/media/media.module new file mode 100644 index 0000000..f5eb091 --- /dev/null +++ b/core/modules/media/media.module @@ -0,0 +1,91 @@ +' . t('About') . ''; + $output .= '

' . t('The Media module manages the creation, editing, deletion, settings and display of media. Items are typically images, documents, slideshows, YouTube videos, Tweets, Instagram photos, etc. You can reference media items from any other content on your site. For more information, see the online documentation for the Media module.', [':media' => 'https://www.drupal.org/docs/8/core/modules/media']) . '

'; + $output .= '

' . t('Uses') . '

'; + $output .= '
'; + $output .= '
' . t('Creating media items') . '
'; + $output .= '
' . t('When a new media item is created, the Media module records basic information about it, including the author, date of creation, and the Media type. It also manages the publishing options, which define whether or not the item is published. Default settings can be configured for each type of media on your site.', [':media-type' => Url::fromRoute('entity.media_type.collection')->toString()]) . '
'; + $output .= '
' . t('Creating custom media types') . '
'; + $output .= '
' . t('The Media module gives users with the Administer media types permission the ability to create new media types in addition to the default ones already configured. Each media type has an associated media handler (such as the image handler) which support thumbnail generation and metadata extraction. Fields managed by the Field module may be added for storing that metadata, such as width and height, as well as any other associated values.', [':media-new' => Url::fromRoute('entity.media_type.add_form')->toString(), ':field' => Url::fromRoute('help.page', ['name' => 'field'])->toString()]) . '
'; + $output .= '
' . t('Creating revisions') . '
'; + $output .= '
' . t('The Media module also enables you to create multiple versions of any media item, and revert to older versions using the Revision information settings.') . '
'; + $output .= '
' . t('User permissions') . '
'; + $output .= '
' . t('The Media module makes a number of permissions available, which can be set by role on the permissions page.', [':permissions' => Url::fromRoute('user.admin_permissions', [], ['fragment' => 'module-media'])->toString()]) . '
'; + $output .= '
'; + return $output; + } +} + +/** + * Implements hook_theme(). + */ +function media_theme() { + return [ + 'media' => [ + 'render element' => 'elements', + 'file' => 'media.theme.inc', + 'template' => 'media', + ], + ]; +} + +/** + * Implements hook_entity_operation_alter(). + * + * @todo: This can be removed when https://www.drupal.org/node/2836384 is fixed. + */ +function media_entity_operation_alter(array &$operations, EntityInterface $entity) { + if ($entity instanceof FieldConfig && $entity->getTargetEntityTypeId() == 'media') { + /** @var \Drupal\media\MediaTypeInterface $media_type */ + $media_type = \Drupal::entityTypeManager()->getStorage('media_type')->load($entity->getTargetBundle()); + if ($entity->id() == 'media.' . $media_type->id() . '.' . $media_type->getHandler()->getConfiguration()['source_field']) { + unset($operations['delete']); + } + } +} + +/** + * Implements hook_entity_access(). + */ +function media_entity_access(EntityInterface $entity, $operation, AccountInterface $account) { + if ($operation == 'delete' && $entity instanceof FieldConfig && $entity->getTargetEntityTypeId() == 'media') { + /** @var \Drupal\media\MediaTypeInterface $media_type */ + $media_type = \Drupal::entityTypeManager()->getStorage('media_type')->load($entity->getTargetBundle()); + return AccessResult::forbiddenIf($entity->id() == 'media.' . $media_type->id() . '.' . $media_type->getHandler()->getConfiguration()['source_field']); + } + return AccessResult::neutral(); +} + +/** + * Implements hook_theme_suggestions_HOOK(). + */ +function media_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; +} diff --git a/core/modules/media/media.permissions.yml b/core/modules/media/media.permissions.yml new file mode 100644 index 0000000..f0d41a3 --- /dev/null +++ b/core/modules/media/media.permissions.yml @@ -0,0 +1,26 @@ +administer media: + title: 'Administer media' + restrict access: TRUE + +administer media types: + title: 'Administer media types' + 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' + restrict access: TRUE + +create media: + title: 'Create media' diff --git a/core/modules/media/media.routing.yml b/core/modules/media/media.routing.yml new file mode 100644 index 0000000..33709b9 --- /dev/null +++ b/core/modules/media/media.routing.yml @@ -0,0 +1,6 @@ +entity.media.multiple_delete_confirm: + path: '/admin/content/media/delete' + defaults: + _form: '\Drupal\media\Form\DeleteMultiple' + requirements: + _permission: 'delete any media' diff --git a/core/modules/media/media.services.yml b/core/modules/media/media.services.yml new file mode 100644 index 0000000..730e2d8 --- /dev/null +++ b/core/modules/media/media.services.yml @@ -0,0 +1,8 @@ +services: + plugin.manager.media.handler: + class: Drupal\media\MediaHandlerManager + parent: default_plugin_manager + + media.thumbnail_handler: + class: Drupal\media\MediaThumbnailHandler + arguments: ['@entity_type.manager', '@string_translation'] diff --git a/core/modules/media/media.theme.inc b/core/modules/media/media.theme.inc new file mode 100644 index 0000000..0b1bbc9 --- /dev/null +++ b/core/modules/media/media.theme.inc @@ -0,0 +1,28 @@ +label(); + + // Helpful $content variable for templates. + foreach (Element::children($variables['elements']) as $key) { + $variables['content'][$key] = $variables['elements'][$key]; + } +} diff --git a/core/modules/media/media.tokens.inc b/core/modules/media/media.tokens.inc new file mode 100644 index 0000000..410a136 --- /dev/null +++ b/core/modules/media/media.tokens.inc @@ -0,0 +1,177 @@ + 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 type'), + ]; + $media['bundle-name'] = [ + 'name' => t('Media type name'), + 'description' => t('The human-readable name of the media type.'), + ]; + $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.'), + ]; + $media['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_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\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->label(); + 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': + $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/src/Annotation/MediaHandler.php b/core/modules/media/src/Annotation/MediaHandler.php new file mode 100644 index 0000000..70001a6 --- /dev/null +++ b/core/modules/media/src/Annotation/MediaHandler.php @@ -0,0 +1,62 @@ +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 getHandler() { + return $this->bundle->entity->getHandler(); + } + + /** + * {@inheritdoc} + */ + public function preSave(EntityStorageInterface $storage) { + parent::preSave($storage); + + // Try to set fields provided by the handler and mapped in bundle config. + foreach ($this->bundle->entity->getFieldMap() as $source_field => $destination_field) { + $this->getHandler()->mapFieldValue($this, $source_field, $destination_field); + } + + // Try to set a default name for this media if no label is provided. + if (!$this->label()) { + $this->set('name', $this->getHandler()->getDefaultName($this)); + } + + // Set thumbnail. + if (!$this->get('thumbnail')->entity) { + \Drupal::service('media.thumbnail_handler')->setThumbnail($this); + } + } + + /** + * {@inheritdoc} + */ + public function postSave(EntityStorageInterface $storage, $update = TRUE) { + parent::postSave($storage, $update); + if (!$update && $this->bundle->entity->getQueueThumbnailDownloadsStatus()) { + $queue = \Drupal::queue('media_entity_thumbnail'); + $queue->createItem(['id' => $this->id()]); + } + } + + /** + * {@inheritdoc} + */ + public function preSaveRevision(EntityStorageInterface $storage, \stdClass $record) { + parent::preSaveRevision($storage, $record); + + if (!$this->isNewRevision() && isset($this->original) && empty($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->getHandler()->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('Name')) + ->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('Authored by')) + ->setDescription(t('The username of the media publisher.')) + ->setRevisionable(TRUE) + ->setDefaultValueCallback(static::class . '::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('Authored on')) + ->setDescription(t('The time the media item 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 the media item was last edited.')) + ->setTranslatable(TRUE) + ->setRevisionable(TRUE); + + $fields['revision_timestamp'] = BaseFieldDefinition::create('created') + ->setLabel(t('Revision timestamp')) + ->setDescription(t('The time 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(static::class . '::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) + ->setDefaultValue('') + ->setDisplayOptions('form', array( + 'type' => 'string_textarea', + 'weight' => 25, + 'settings' => array( + 'rows' => 4, + ), + )); + + return $fields; + } + + /** + * Default value callback for 'uid' base field definition. + * + * @see ::baseFieldDefinitions() + * + * @return int[] + * 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/src/Entity/MediaType.php b/core/modules/media/src/Entity/MediaType.php new file mode 100644 index 0000000..908b837 --- /dev/null +++ b/core/modules/media/src/Entity/MediaType.php @@ -0,0 +1,301 @@ + $this->handlerPluginCollection(), + ]; + } + + /** + * {@inheritdoc} + */ + public static function getLabel(MediaInterface $media) { + $bundle = static::load($media->bundle()); + return $bundle ? $bundle->label() : FALSE; + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + return $this->description; + } + + /** + * {@inheritdoc} + */ + public function setDescription($description) { + $this->description = $description; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getQueueThumbnailDownloadsStatus() { + return $this->queue_thumbnail_downloads; + } + + /** + * {@inheritdoc} + */ + public function setQueueThumbnailDownloadsStatus($queue_thumbnail_downloads) { + $this->queue_thumbnail_downloads = $queue_thumbnail_downloads; + return $this; + } + + /** + * {@inheritdoc} + */ + public function getHandler() { + return $this->handlerPluginCollection()->get($this->handler); + } + + /** + * Returns handler lazy plugin collection. + * + * @return \Drupal\Core\Plugin\DefaultSingleLazyPluginCollection + * The tag plugin collection. + */ + protected function handlerPluginCollection() { + if (!$this->handlerPluginCollection) { + $this->handlerPluginCollection = new DefaultSingleLazyPluginCollection(\Drupal::service('plugin.manager.media.handler'), $this->handler, $this->handler_configuration); + } + return $this->handlerPluginCollection; + } + + /** + * {@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; + return $this; + } + + /** + * {@inheritdoc} + */ + public function preSave(EntityStorageInterface $storage) { + // If the handler uses a source field, we'll need to store its name before + // saving. We'd need to double-save if we did this in postSave(). + $handler = $this->getHandler(); + if ($this->auto_create_source_field && $handler instanceof SourceFieldInterface) { + $field_storage = $handler->getSourceField($this)->getFieldStorageDefinition(); + // If the field storage is a new (unsaved) config entity, save it. + if ($field_storage instanceof FieldStorageConfigInterface && $field_storage->isNew()) { + $field_storage->save(); + } + } + + parent::preSave($storage); + } + + /** + * {@inheritdoc} + */ + public function postSave(EntityStorageInterface $storage, $update = TRUE) { + parent::postSave($storage, $update); + + // If the handler is using a source field, we may need to save it if it's + // new. The field storage is guaranteed to exist already because preSave() + // took care of that. + $handler = $this->getHandler(); + if ($this->auto_create_source_field && $handler instanceof SourceFieldInterface) { + $field = $handler->getSourceField($this); + + // If the field is new, save it and add it to this bundle's view and form + // displays. + if ($field->isNew()) { + // Ensure the field is saved correctly before adding it to the displays. + $field->save(); + + $entity_type = $field->getTargetEntityTypeId(); + $bundle = $field->getTargetBundle(); + + if ($field->isDisplayConfigurable('form')) { + // Use the default widget and settings. + $component = \Drupal::service('plugin.manager.field.widget') + ->prepareConfiguration($field->getType(), []); + + entity_get_form_display($entity_type, $bundle, 'default') + ->setComponent($field->getName(), $component) + ->save(); + } + if ($field->isDisplayConfigurable('view')) { + // Use the default formatter and settings. + $component = \Drupal::service('plugin.manager.field.formatter') + ->prepareConfiguration($field->getType(), []); + + entity_get_display($entity_type, $bundle, 'default') + ->setComponent($field->getName(), $component) + ->save(); + } + } + } + } + + /** + * {@inheritdoc} + */ + public function getFieldMap() { + return $this->field_map; + } + +} diff --git a/core/modules/media/src/Form/DeleteMultiple.php b/core/modules/media/src/Form/DeleteMultiple.php new file mode 100644 index 0000000..4832f41 --- /dev/null +++ b/core/modules/media/src/Form/DeleteMultiple.php @@ -0,0 +1,197 @@ +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($this->currentUser()->id()); + if (empty($this->entityInfo)) { + return new RedirectResponse($this->getCancelUrl()->setAbsolute()->toString()); + } + /** @var \Drupal\media\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 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, + ]; + return parent::buildForm($form, $form_state); + } + + /** + * {@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\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')->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')->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/src/Form/MediaTypeDeleteConfirmForm.php b/core/modules/media/src/Form/MediaTypeDeleteConfirmForm.php new file mode 100644 index 0000000..5bf9d63 --- /dev/null +++ b/core/modules/media/src/Form/MediaTypeDeleteConfirmForm.php @@ -0,0 +1,68 @@ +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) { + $form['#title'] = $this->getQuestion(); + $form['description'] = [ + '#type' => 'inline_template', + '#template' => '

{{ message }}

', + '#context' => [ + 'message' => $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()]), + ], + ]; + + return $form; + } + + return parent::buildForm($form, $form_state); + } + +} diff --git a/core/modules/media/src/MediaAccessController.php b/core/modules/media/src/MediaAccessController.php new file mode 100644 index 0000000..9ddd60a --- /dev/null +++ b/core/modules/media/src/MediaAccessController.php @@ -0,0 +1,46 @@ +hasPermission('administer media')) { + return AccessResult::allowed()->cachePerPermissions(); + } + + $is_owner = ($account->id() && $account->id() == $entity->getOwnerId()); + switch ($operation) { + case 'view': + return AccessResult::allowedIf($account->hasPermission('view media') && $entity->isPublished())->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/src/MediaForm.php b/core/modules/media/src/MediaForm.php new file mode 100644 index 0000000..7bd1cea --- /dev/null +++ b/core/modules/media/src/MediaForm.php @@ -0,0 +1,184 @@ +getEntity()->isNew()) { + $this->entity->setOwnerId($this->currentUser()->id()); + $this->entity->setCreatedTime($this->time->getRequestTime()); + } + } + + /** + * {@inheritdoc} + */ + public function form(array $form, FormStateInterface $form_state) { + $form = parent::form($form, $form_state); + /** @var \Drupal\media\MediaTypeInterface $bundle_entity */ + $bundle_entity = $this->entity->bundle->entity; + + if ($this->operation == 'edit') { + $form['#title'] = $this->t('Edit %bundle_label @label', [ + '%bundle_label' => $bundle_entity->label(), + '@label' => $this->entity->label(), + ]); + } + + // 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/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\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\MediaForm::form() + */ + public function updateStatus($entity_type_id, MediaInterface $media, array $form, FormStateInterface $form_state) { + $element = $form_state->getTriggeringElement(); + if (!empty($element['#published_status'])) { + $media->setPublished(); + } + else { + $media->setUnpublished(); + } + } + + /** + * {@inheritdoc} + */ + public function save(array $form, FormStateInterface $form_state) { + $saved = parent::save($form, $form_state); + $context = ['@type' => $this->entity->bundle(), '%label' => $this->entity->label()]; + $logger = $this->logger('media'); + $t_args = ['@type' => $this->entity->bundle->entity->label(), '%label' => $this->entity->label()]; + + if ($saved === SAVED_NEW) { + $logger->notice('@type: added %label.', $context); + drupal_set_message($this->t('@type %label has been created.', $t_args)); + } + else { + $logger->notice('@type: updated %label.', $context); + drupal_set_message($this->t('@type %label has been updated.', $t_args)); + } + + $form_state->setRedirectUrl($this->entity->toUrl('canonical')); + return $saved; + } + +} diff --git a/core/modules/media/src/MediaHandlerBase.php b/core/modules/media/src/MediaHandlerBase.php new file mode 100644 index 0000000..e2c7294 --- /dev/null +++ b/core/modules/media/src/MediaHandlerBase.php @@ -0,0 +1,354 @@ +entityTypeManager = $entity_type_manager; + $this->entityFieldManager = $entity_field_manager; + $this->configFactory = $config_factory; + $this->fieldTypeManager = $field_type_manager; + $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'), + $container->get('plugin.manager.field.field_type') + ); + } + + /** + * {@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 [ + 'source_field' => '', + ]; + } + + /** + * {@inheritdoc} + */ + public function getDefaultThumbnail() { + return $this->configFactory->get('media.settings')->get('icon_base') . '/generic.png'; + } + + /** + * {@inheritdoc} + */ + public function getLabel() { + return $this->label; + } + + /** + * {@inheritdoc} + */ + public function attachConstraints(MediaInterface $media) { + } + + /** + * {@inheritdoc} + */ + public function calculateDependencies() { + return []; + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + // If there are existing fields to choose from, allow the user to reuse one. + $options = []; + foreach ($this->entityFieldManager->getFieldStorageDefinitions('media') as $field_name => $field) { + $allowed_type = in_array($field->getType(), $this->pluginDefinition['allowed_field_types'], TRUE); + if ($allowed_type && !$field->isBaseField()) { + $options[$field_name] = $field->getLabel(); + } + } + + $form['source_field'] = [ + '#type' => 'select', + '#title' => $this->t('Field with source information'), + '#default_value' => $this->configuration['source_field'], + '#empty_option' => $this->t('- Create -'), + '#options' => $options, + '#description' => $this->t('Select the field that will store essential information about the media item. If "Create" is selected a new field will be automatically created.'), + ]; + + if (!$options && $form_state->get('operation') === 'add') { + $form['source_field']['#access'] = FALSE; + $field_definition = $this->fieldTypeManager->getDefinition(reset($this->pluginDefinition['allowed_field_types'])); + $form['source_field_message'] = [ + '#markup' => $this->t('%field_name field will be automatically created on this type to store the essential information about the media item.', [ + '%field_name' => $field_definition['label'], + ]), + ]; + } + elseif ($form_state->get('operation') === 'edit') { + $form['source_field']['#access'] = FALSE; + $fields = $this->entityFieldManager->getFieldDefinitions('media', $form_state->get('type')->id()); + $form['source_field_message'] = [ + '#markup' => $this->t('%field_name field is used to store the essential information about the media item.', [ + '%field_name' => $fields[$this->configuration['source_field']]->getLabel(), + ]), + ]; + } + + return $form; + } + + /** + * {@inheritdoc} + */ + public function validateConfigurationForm(array &$form, FormStateInterface $form_state) { + } + + /** + * {@inheritdoc} + */ + public function submitConfigurationForm(array &$form, FormStateInterface $form_state) { + foreach ($form_state->getValues() as $config_key => $config_value) { + if (isset($this->configuration[$config_key])) { + $this->configuration[$config_key] = $config_value; + } + } + } + + /** + * {@inheritdoc} + */ + public function getDefaultName(MediaInterface $media) { + return 'media:' . $media->bundle() . ':' . $media->uuid(); + } + + /** + * {@inheritdoc} + */ + public function getSourceField(MediaTypeInterface $type, $retry = TRUE) { + // If we don't know the name of the source field, we definitely need to + // create it. + if (empty($this->configuration['source_field'])) { + return $this->createSourceField($type); + } + // Even if we do know the name of the source field, there is no guarantee + // that it already exists. So check for the field and create it if needed. + $field = $this->configuration['source_field']; + $fields = $this->entityFieldManager->getFieldDefinitions('media', $type->id()); + return isset($fields[$field]) ? $fields[$field] : $this->createSourceField($type); + } + + /** + * Returns the source field storage definition. + * + * @return \Drupal\Core\Field\FieldStorageDefinitionInterface + * The field storage definition. Will be unsaved if new. + */ + protected function getSourceFieldStorage() { + // If we don't know the name of the source field, we definitely need to + // create its storage. + if (empty($this->configuration['source_field'])) { + $storage = $this->createSourceFieldStorage(); + $this->configuration['source_field'] = $storage->getName(); + return $storage; + } + // Even if we do know the name of the source field, we cannot guarantee that + // its storage exists. So check for the storage and create it if needed. + $field = $this->configuration['source_field']; + $fields = $this->entityFieldManager->getFieldStorageDefinitions('media'); + return isset($fields[$field]) ? $fields[$field] : $this->createSourceFieldStorage(); + } + + /** + * Creates the source field storage definition. + * + * By default, the first field type listed in the plugin definition's + * allowed_field_types array will be the generated field's type. + * + * @return \Drupal\field\FieldStorageConfigInterface + * The unsaved field storage definition. + */ + protected function createSourceFieldStorage() { + return $this->entityTypeManager + ->getStorage('field_storage_config') + ->create([ + 'entity_type' => 'media', + 'field_name' => $this->getSourceFieldName(), + 'type' => reset($this->pluginDefinition['allowed_field_types']), + ]); + } + + /** + * Creates the source field definition for a type. + * + * @param \Drupal\media\MediaTypeInterface $type + * The media type. + * + * @return \Drupal\field\FieldConfigInterface + * The unsaved field definition. The field storage definition, if new, + * should also be unsaved. + */ + protected function createSourceField(MediaTypeInterface $type) { + return $this->entityTypeManager + ->getStorage('field_config') + ->create([ + 'field_storage' => $this->getSourceFieldStorage(), + 'bundle' => $type->id(), + 'label' => $this->pluginDefinition['label'], + 'required' => TRUE, + ]); + } + + /** + * Determine the name of the source field. + * + * @return string + * The source field name. If one is already stored in configuration, it is + * returned. Otherwise, a new, unused one is generated. + */ + protected function getSourceFieldName() { + if ($this->configuration['source_field']) { + return $this->configuration['source_field']; + } + + $base_id = 'field_media_' . $this->getPluginId(); + $tries = 0; + $storage = $this->entityTypeManager->getStorage('field_storage_config'); + + // Iterate at least once, until no field with the generated ID is found. + do { + $id = $base_id; + // If we've tried before, increment and append the suffix. + if ($tries) { + $id .= '_' . $tries; + } + $field = $storage->load('media.' . $id); + $tries++; + } while ($field); + + return $id; + } + + /** + * {@inheritdoc} + */ + public function mapFieldValue(MediaInterface $media, $source_field, $destination_field) { + // Only save value in entity field if empty. Do not overwrite existing + // data. + if ($media->hasField($destination_field) && $media->get($destination_field)->isEmpty() && ($value = $this->getField($media, $source_field))) { + $media->set($destination_field, $value); + } + } + + /** + * {@inheritdoc} + */ + public function getSourceValue(MediaInterface $media) { + $source_field = $this->configuration['source_field']; + + if (empty($source_field)) { + throw new \RuntimeException('Source field for media handler is not defined.'); + } + + // Source value is stored as the main property of the source field in 99% of + // the cases so return that. 1% can override this function and tweak the + // logic. + /** @var \Drupal\Core\Field\FieldItemInterface $field_item */ + $field_item = $media->get($source_field)->first(); + return $field_item->get($field_item->mainPropertyName()); + } + +} diff --git a/core/modules/media/src/MediaHandlerInterface.php b/core/modules/media/src/MediaHandlerInterface.php new file mode 100644 index 0000000..2c46ac1 --- /dev/null +++ b/core/modules/media/src/MediaHandlerInterface.php @@ -0,0 +1,110 @@ +alterInfo('media_handler_info'); + $this->setCacheBackend($cache_backend, 'media_handler_plugins'); + } + +} diff --git a/core/modules/media/src/MediaInterface.php b/core/modules/media/src/MediaInterface.php new file mode 100644 index 0000000..91ec053 --- /dev/null +++ b/core/modules/media/src/MediaInterface.php @@ -0,0 +1,43 @@ +fileStorage = $entity_type_manager->getStorage('file'); + $this->stringTranslation = $translation; + } + + /** + * {@inheritdoc} + */ + public function setThumbnail(MediaInterface $media) { + // If thumbnail fetching should be queued then temporary use default + // thumbnail or fetch it immediately otherwise. + if ($media->bundle->entity->getQueueThumbnailDownloadsStatus() && $media->isNew()) { + $thumbnail_uri = $media->getHandler()->getDefaultThumbnail(); + } + else { + $thumbnail_uri = $media->getHandler()->getThumbnail($media); + } + $existing = $this->fileStorage->getQuery() + ->condition('uri', $thumbnail_uri) + ->execute(); + + if ($existing) { + $media->thumbnail->target_id = reset($existing); + } + else { + /** @var \Drupal\file\FileInterface $file */ + $file = $this->fileStorage->create(['uri' => $thumbnail_uri]); + if ($owner = $media->getOwner()) { + $file->setOwner($owner); + } + $file->setPermanent(); + $file->save(); + $media->thumbnail->target_id = $file->id(); + } + + $media->thumbnail->alt = $this->t('Thumbnail'); + $media->thumbnail->title = $media->label(); + } + +} diff --git a/core/modules/media/src/MediaThumbnailHandlerInterface.php b/core/modules/media/src/MediaThumbnailHandlerInterface.php new file mode 100644 index 0000000..67d976c --- /dev/null +++ b/core/modules/media/src/MediaThumbnailHandlerInterface.php @@ -0,0 +1,18 @@ +handlerManager = $handler_manager; + $this->entityFieldManager = $entity_field_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('plugin.manager.media.handler'), + $container->get('entity_field.manager') + ); + } + + /** + * Ajax callback triggered by the type provider select element. + */ + public function ajaxHandlerData(array $form, FormStateInterface $form_state) { + $response = new AjaxResponse(); + $response->addCommand(new ReplaceCommand('#handler-dependent', $form['handler_dependent'])); + return $response; + } + + /** + * {@inheritdoc} + */ + public function form(array $form, FormStateInterface $form_state) { + $form = parent::form($form, $form_state); + + // Handler is not set when the entity is initially created. + /** @var \Drupal\media\MediaHandlerInterface $handler */ + $handler = $this->entity->get('handler') ? $this->entity->getHandler() : NULL; + + if ($this->operation === 'add') { + $form['#title'] = $this->t('Add media type'); + } + + $form['label'] = [ + '#title' => $this->t('Name'), + '#type' => 'textfield', + '#default_value' => $this->entity->label(), + '#description' => $this->t('The human-readable name of this media type.'), + '#required' => TRUE, + '#size' => 30, + ]; + + $form['id'] = [ + '#type' => 'machine_name', + '#default_value' => $this->entity->id(), + '#maxlength' => 32, + '#disabled' => !$this->entity->isNew(), + '#machine_name' => [ + 'exists' => [MediaType::class, 'load'], + ], + '#description' => $this->t('A unique machine-readable name for this media type.'), + ]; + + $form['description'] = [ + '#title' => $this->t('Description'), + '#type' => 'textarea', + '#default_value' => $this->entity->getDescription(), + '#description' => $this->t('Describe this media type. The text will be displayed on the Add new media page.'), + ]; + + $plugins = $this->handlerManager->getDefinitions(); + $options = []; + foreach ($plugins as $plugin_id => $definition) { + $options[$plugin_id] = $definition['label']; + } + + $form['handler_dependent'] = [ + '#type' => 'container', + '#attributes' => ['id' => 'handler-dependent'], + ]; + + $form['handler_dependent']['handler'] = [ + '#type' => 'select', + '#title' => $this->t('Handler'), + '#default_value' => $handler ? $handler->getPluginId() : NULL, + '#options' => $options, + '#description' => $this->t('Media handler that is responsible for additional logic related to this media type.'), + '#ajax' => ['callback' => '::ajaxHandlerData'], + '#required' => TRUE, + ]; + + if (!$handler) { + $form['type']['#empty_option'] = $this->t('- Select handler -'); + } + + if ($handler) { + // Media handler plugin configuration. + $form['handler_dependent']['handler_configuration'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Handler configuration'), + '#tree' => TRUE, + ]; + + $form['handler_dependent']['handler_configuration'] = $handler->buildConfigurationForm($form['handler_dependent']['handler_configuration'], $this->getHandlerSubFormState($form, $form_state)); + } + + // Field mapping configuration. + $form['handler_dependent']['field_map'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Field mapping'), + '#tree' => TRUE, + 'description' => [ + '#markup' => '

' . $this->t('Media handlers can provide metadata fields such as title, caption, size information, credits, etc. Media 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.') . '

', + ], + ]; + + if (empty($handler) || empty($handler->getProvidedFields())) { + $form['handler_dependent']['field_map']['#access'] = FALSE; + } + else { + $options = ['_none' => $this->t('- Skip field -')]; + foreach ($this->entityFieldManager->getFieldDefinitions('media', $this->entity->id()) as $field_name => $field) { + if (!($field instanceof BaseFieldDefinition) || $field_name == 'name') { + $options[$field_name] = $field->getLabel(); + } + } + + $field_map = $this->entity->getFieldMap(); + foreach ($handler->getProvidedFields() as $field_name => $field_definition) { + $form['handler_dependent']['field_map'][$field_name] = [ + '#type' => 'select', + '#title' => $field_definition['label'], + '#options' => $options, + '#default_value' => isset($field_map[$field_name]) ? $field_map[$field_name] : '_none', + ]; + } + } + + $form['additional_settings'] = [ + '#type' => 'vertical_tabs', + '#attached' => [ + 'library' => ['media/media_type_form'], + ], + ]; + + $form['workflow'] = [ + '#type' => 'details', + '#title' => $this->t('Publishing options'), + '#group' => 'additional_settings', + ]; + + $form['workflow']['options'] = [ + '#type' => 'checkboxes', + '#title' => $this->t('Default options'), + '#default_value' => $this->getWorkflowOptions(), + '#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('Media will be automatically published when created.'); + $form['workflow']['options']['new_revision']['#description'] = $this->t('Automatically create new revisions. 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 = call_user_func( + ['\Drupal\language\Entity\ContentLanguageSettings', 'loadByEntityTypeBundle'], + 'media', + $this->entity->id() + ); + + $form['language']['language_configuration'] = [ + '#type' => 'language_configuration', + '#entity_information' => [ + 'entity_type' => 'media', + 'bundle' => $this->entity->id(), + ], + '#default_value' => $language_configuration, + ]; + } + + return $form; + } + + /** + * Prepares workflow options to be used in the 'checkboxes' form element. + * + * @return array + * Array of options ready to be used in #options. + */ + protected function getWorkflowOptions() { + $workflow_options = [ + 'status' => $this->entity->getStatus(), + 'new_revision' => $this->entity->shouldCreateNewRevision(), + 'queue_thumbnail_downloads' => $this->entity->getQueueThumbnailDownloadsStatus(), + ]; + // Prepare workflow options to be used for 'checkboxes' form element. + $keys = array_keys(array_filter($workflow_options)); + return array_combine($keys, $keys); + } + + /** + * Gets sub-form state for the handler configuration sub-form. + * + * @param array $form + * Full form array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * Parent form state. + * + * @return \Drupal\Core\Form\SubFormStateInterface + * Sub-form state for the handler configuration form. + */ + protected function getHandlerSubFormState(array $form, FormStateInterface $form_state) { + return SubformState::createForSubform($form['handler_dependent']['handler_configuration'], $form, $form_state) + ->set('operation', $this->operation) + ->set('type', $this->entity); + } + + /** + * {@inheritdoc} + */ + public function validateForm(array &$form, FormStateInterface $form_state) { + parent::validateForm($form, $form_state); + + if ($form['handler_dependent']['handler_configuration']) { + // Let the selected plugin validate its settings. + $this->entity->getHandler()->validateConfigurationForm($form['handler_dependent']['handler_configuration'], $this->getHandlerSubFormState($form, $form_state)); + } + } + + /** + * {@inheritdoc} + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + $form_state->setValue('field_map', array_filter( + $form_state->getValue('field_map', []), + function ($item) { + return $item != '_none'; + } + )); + + parent::submitForm($form, $form_state); + + $this->entity->setQueueThumbnailDownloadsStatus((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'])); + + if ($form['handler_dependent']['handler_configuration']) { + // Let the selected plugin save its settings. + $this->entity->getHandler()->submitConfigurationForm($form['handler_dependent']['handler_configuration'], $this->getHandlerSubFormState($form, $form_state)); + } + } + + /** + * {@inheritdoc} + */ + protected function actions(array $form, FormStateInterface $form_state) { + $actions = parent::actions($form, $form_state); + $actions['submit']['#value'] = $this->t('Save'); + $actions['delete']['#value'] = $this->t('Delete'); + $actions['delete']['#access'] = $this->entity->access('delete'); + return $actions; + } + + /** + * {@inheritdoc} + */ + public function save(array $form, FormStateInterface $form_state) { + $this->entity->set('auto_create_source_field', TRUE); + $status = parent::save($form, $form_state); + $bundle = $this->entity; + + $t_args = ['%name' => $bundle->label()]; + if ($status == SAVED_UPDATED) { + drupal_set_message($this->t('The media type %name has been updated.', $t_args)); + } + elseif ($status == SAVED_NEW) { + drupal_set_message($this->t('The media type %name has been added.', $t_args)); + $this->logger('media')->notice('Added type %name.', $t_args); + } + + // Override the "status" base field default value, for this bundle. + $fields = $this->entityFieldManager->getFieldDefinitions('media', $bundle->id()); + /** @var \Drupal\media\MediaInterface $media */ + $media = $this->entityTypeManager->getStorage('media')->create(['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/src/MediaTypeInterface.php b/core/modules/media/src/MediaTypeInterface.php new file mode 100644 index 0000000..9407e04 --- /dev/null +++ b/core/modules/media/src/MediaTypeInterface.php @@ -0,0 +1,76 @@ +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']['data'] = ['#markup' => $entity->getDescription()]; + return $row + parent::buildRow($entity); + } + + /** + * {@inheritdoc} + */ + public function render() { + $build = parent::render(); + $build['table']['#empty'] = $this->t('No media types available. Add media type.', [ + '@link' => Url::fromRoute('entity.media_type.add_form')->toString(), + ]); + return $build; + } + +} diff --git a/core/modules/media/src/MediaViewsData.php b/core/modules/media/src/MediaViewsData.php new file mode 100644 index 0000000..f186871 --- /dev/null +++ b/core/modules/media/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\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($object ? [$object] : []); + } + + /** + * {@inheritdoc} + */ + public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { + /** @var \Drupal\media\MediaInterface $object */ + return $object->access('delete', $account, $return_as_object); + } + +} diff --git a/core/modules/media/src/Plugin/Action/PublishMedia.php b/core/modules/media/src/Plugin/Action/PublishMedia.php new file mode 100644 index 0000000..1c22e4f --- /dev/null +++ b/core/modules/media/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\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/src/Plugin/Action/SaveMedia.php b/core/modules/media/src/Plugin/Action/SaveMedia.php new file mode 100644 index 0000000..4a20360 --- /dev/null +++ b/core/modules/media/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\MediaInterface $object */ + return $object->access('update', $account, $return_as_object); + } + +} diff --git a/core/modules/media/src/Plugin/Action/UnpublishMedia.php b/core/modules/media/src/Plugin/Action/UnpublishMedia.php new file mode 100644 index 0000000..05d12bf --- /dev/null +++ b/core/modules/media/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\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/src/Plugin/Field/FieldFormatter/MediaThumbnailFormatter.php b/core/modules/media/src/Plugin/Field/FieldFormatter/MediaThumbnailFormatter.php new file mode 100644 index 0000000..2f7d8aa --- /dev/null +++ b/core/modules/media/src/Plugin/Field/FieldFormatter/MediaThumbnailFormatter.php @@ -0,0 +1,190 @@ +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_type.manager')->getStorage('image_style'), + $container->get('renderer') + ); + } + + /** + * {@inheritdoc} + * + * This has to be overridden 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\MediaInterface[] $media */ + 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/src/Plugin/QueueWorker/ThumbnailDownloader.php b/core/modules/media/src/Plugin/QueueWorker/ThumbnailDownloader.php new file mode 100644 index 0000000..161a88e --- /dev/null +++ b/core/modules/media/src/Plugin/QueueWorker/ThumbnailDownloader.php @@ -0,0 +1,70 @@ +thumbnailHandler = $thumbnail_handler; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('media.thumbnail_handler') + ); + } + + /** + * {@inheritdoc} + */ + public function processItem($data) { + /** @var \Drupal\media\MediaInterface $entity */ + if ($entity = Media::load($data['id'])) { + $this->thumbnailHandler->setThumbnail($entity); + $entity->save(); + } + } + +} diff --git a/core/modules/media/src/Plugin/media/Handler/File.php b/core/modules/media/src/Plugin/media/Handler/File.php new file mode 100644 index 0000000..531896b --- /dev/null +++ b/core/modules/media/src/Plugin/media/Handler/File.php @@ -0,0 +1,113 @@ + ['label' => $this->t('MIME type')], + 'size' => ['label' => $this->t('Size')], + ]; + } + + /** + * {@inheritdoc} + */ + public function getField(MediaInterface $media, $name) { + $file = $this->getSourceFile($media); + + switch ($name) { + case 'mime': + return $file->getMimeType() ?: FALSE; + + case 'size': + $size = $file->getSize(); + return is_numeric($size) ? $size : FALSE; + } + + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function getThumbnail(MediaInterface $media) { + $file = $this->getSourceFile($media); + $icon_base = $this->configFactory->get('media.settings')->get('icon_base'); + $thumbnail = FALSE; + + // We try to magically use the most specific icon present in the $icon_base + // directory, based on the MIME information. For instance, if an icon file + // named "pdf.png" is present, it will be used if the file matches this + // MIME type. + 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. + return $this->getSourceFile($media)->getFilename(); + } + + /** + * Get source field file entity. + * + * @param \Drupal\media\MediaInterface $media + * The media entity. + * + * @return \Drupal\file\FileInterface + * The file entity present in the source field. + * + * @throws \RuntimeException + * If the source field for the handler is not defined, or if the + * source field does not contain a file entity. + */ + protected function getSourceFile(MediaInterface $media) { + $source_field = $this->configuration['source_field']; + + if (empty($source_field)) { + throw new \RuntimeException('Source field for media file handler is not defined.'); + } + + $file = $media->get($source_field)->entity; + if (empty($file)) { + throw new \RuntimeException('The source field does not contain a file entity.'); + } + + return $file; + } + +} diff --git a/core/modules/media/src/Plugin/views/wizard/Media.php b/core/modules/media/src/Plugin/views/wizard/Media.php new file mode 100644 index 0000000..ed0d46d --- /dev/null +++ b/core/modules/media/src/Plugin/views/wizard/Media.php @@ -0,0 +1,88 @@ + [ + '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/src/Plugin/views/wizard/MediaRevision.php b/core/modules/media/src/Plugin/views/wizard/MediaRevision.php new file mode 100644 index 0000000..68cb14f --- /dev/null +++ b/core/modules/media/src/Plugin/views/wizard/MediaRevision.php @@ -0,0 +1,99 @@ + [ + '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/src/SourceFieldInterface.php b/core/modules/media/src/SourceFieldInterface.php new file mode 100644 index 0000000..1ee3294 --- /dev/null +++ b/core/modules/media/src/SourceFieldInterface.php @@ -0,0 +1,37 @@ + + {# + 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/tests/modules/media_test_handler/config/schema/media_test_handler.schema.yml b/core/modules/media/tests/modules/media_test_handler/config/schema/media_test_handler.schema.yml new file mode 100644 index 0000000..63abccd --- /dev/null +++ b/core/modules/media/tests/modules/media_test_handler/config/schema/media_test_handler.schema.yml @@ -0,0 +1,7 @@ +media.handler.test: + type: media.handler.field_aware + label: 'Test handler configuration' + mapping: + test_config_value: + type: string + label: 'Test config value' diff --git a/core/modules/media/tests/modules/media_test_handler/media_test_handler.info.yml b/core/modules/media/tests/modules/media_test_handler/media_test_handler.info.yml new file mode 100644 index 0000000..e29fe8c --- /dev/null +++ b/core/modules/media/tests/modules/media_test_handler/media_test_handler.info.yml @@ -0,0 +1,6 @@ +name: 'Test media handler' +type: module +description: 'Provides test media handler to test configuration forms.' +core: 8.x +package: Testing +version: VERSION diff --git a/core/modules/media/tests/modules/media_test_handler/src/Plugin/media/Handler/Test.php b/core/modules/media/tests/modules/media_test_handler/src/Plugin/media/Handler/Test.php new file mode 100644 index 0000000..c43a0f9 --- /dev/null +++ b/core/modules/media/tests/modules/media_test_handler/src/Plugin/media/Handler/Test.php @@ -0,0 +1,73 @@ +getDefaultThumbnail(); + } + + /** + * {@inheritdoc} + */ + public function getProvidedFields() { + return [ + 'field_1' => [ + 'label' => $this->t('Field 1'), + ], + 'field_2' => [ + 'label' => $this->t('Field 2'), + ], + ]; + } + + /** + * {@inheritdoc} + */ + public function getField(MediaInterface $media, $name) { + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return parent::defaultConfiguration() + [ + 'test_config_value' => 'This is default value.', + ]; + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $form = parent::buildConfigurationForm($form, $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/tests/modules/media_test_type/config/install/media.type.test.yml b/core/modules/media/tests/modules/media_test_type/config/install/media.type.test.yml new file mode 100644 index 0000000..3256baf --- /dev/null +++ b/core/modules/media/tests/modules/media_test_type/config/install/media.type.test.yml @@ -0,0 +1,10 @@ +id: test +label: 'Test type' +description: 'Test type.' +handler: test +handler_configuration: + test_config_value: 'Kakec' +status: true +langcode: en +dependencies: { } +field_map: { } diff --git a/core/modules/media/tests/modules/media_test_type/media_test_type.info.yml b/core/modules/media/tests/modules/media_test_type/media_test_type.info.yml new file mode 100644 index 0000000..b3512de --- /dev/null +++ b/core/modules/media/tests/modules/media_test_type/media_test_type.info.yml @@ -0,0 +1,8 @@ +name: 'Media test type' +type: module +description: 'Provides test type for media entity.' +core: 8.x +package: Testing +version: VERSION +dependencies: + - media_test_handler diff --git a/core/modules/media/tests/modules/media_test_views/config/install/views.view.test_media_bulk_form.yml b/core/modules/media/tests/modules/media_test_views/config/install/views.view.test_media_bulk_form.yml new file mode 100644 index 0000000..dd432bb --- /dev/null +++ b/core/modules/media/tests/modules/media_test_views/config/install/views.view.test_media_bulk_form.yml @@ -0,0 +1,154 @@ +langcode: en +status: true +dependencies: + module: + - media + - user +id: test_media_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-bulk-form diff --git a/core/modules/media/tests/modules/media_test_views/media_test_views.info.yml b/core/modules/media/tests/modules/media_test_views/media_test_views.info.yml new file mode 100644 index 0000000..4962319 --- /dev/null +++ b/core/modules/media/tests/modules/media_test_views/media_test_views.info.yml @@ -0,0 +1,9 @@ +name: 'Media test views' +type: module +description: 'Provides default views for views media tests.' +package: Testing +version: VERSION +core: 8.x +dependencies: + - media + - views diff --git a/core/modules/media/tests/src/Functional/MediaAccessTest.php b/core/modules/media/tests/src/Functional/MediaAccessTest.php new file mode 100644 index 0000000..2bf2766 --- /dev/null +++ b/core/modules/media/tests/src/Functional/MediaAccessTest.php @@ -0,0 +1,100 @@ +testBundle = $this->drupalCreateMediaType(); + } + + /** + * 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/tests/src/Functional/MediaBulkFormTest.php b/core/modules/media/tests/src/Functional/MediaBulkFormTest.php new file mode 100644 index 0000000..41e7b89 --- /dev/null +++ b/core/modules/media/tests/src/Functional/MediaBulkFormTest.php @@ -0,0 +1,115 @@ +testBundle = $this->drupalCreateMediaType(); + + // 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_bulk_form'); + $view->execute(); + $this->assertEquals($view->total_rows, 5); + + // Check the operations are accessible to the logged in user. + $this->drupalGet('test-media-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/tests/src/Functional/MediaFunctionalTestBase.php b/core/modules/media/tests/src/Functional/MediaFunctionalTestBase.php new file mode 100644 index 0000000..a801706 --- /dev/null +++ b/core/modules/media/tests/src/Functional/MediaFunctionalTestBase.php @@ -0,0 +1,96 @@ +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/tests/src/Functional/MediaFunctionalTestTrait.php b/core/modules/media/tests/src/Functional/MediaFunctionalTestTrait.php new file mode 100644 index 0000000..e4f078b --- /dev/null +++ b/core/modules/media/tests/src/Functional/MediaFunctionalTestTrait.php @@ -0,0 +1,48 @@ +randomMachineName()); + } + else { + $id = $values['bundle']; + } + $values += [ + 'id' => $id, + 'label' => $id, + 'handler' => $handler, + 'handler_configuration' => [], + 'field_map' => [], + 'new_revision' => FALSE, + ]; + + $bundle = MediaType::create($values); + $status = $bundle->save(); + + $this->assertEquals($status, SAVED_NEW, 'Media type was created successfully.'); + + return $bundle; + } + +} diff --git a/core/modules/media/tests/src/Functional/MediaUiFunctionalTest.php b/core/modules/media/tests/src/Functional/MediaUiFunctionalTest.php new file mode 100644 index 0000000..c4ff325 --- /dev/null +++ b/core/modules/media/tests/src/Functional/MediaUiFunctionalTest.php @@ -0,0 +1,164 @@ +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(); + + $bundle = $this->drupalCreateMediaType(['new_revision' => FALSE]); + + $this->drupalGet('media/add'); + $assert_session->statusCodeEquals(200); + $assert_session->addressEquals('media/add/' . $bundle->id()); + $assert_session->elementNotExists('css', '#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[0][value]', $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\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[0][value]', $revision_log_message); + $page->pressButton('Save and keep published'); + $assert_session->titleEquals($media_name . ' | Drupal'); + /** @var \Drupal\media\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->drupalCreateMediaType(['description' => $this->randomMachineName(32)]); + + // Test and create a second media bundle. + $second_media_bundle = $this->drupalCreateMediaType(['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/tests/src/FunctionalJavascript/BundleCreationTest.php b/core/modules/media/tests/src/FunctionalJavascript/BundleCreationTest.php new file mode 100644 index 0000000..51c8a6f --- /dev/null +++ b/core/modules/media/tests/src/FunctionalJavascript/BundleCreationTest.php @@ -0,0 +1,98 @@ +drupalGet('admin/structure/media/add'); + $page = $this->getSession()->getPage(); + + // Fill in a label to the bundle. + $page->fillField('label', $label); + // Wait for machine name generation. Default: waitUntilVisible(), does not + // work properly. + $this->getSession() + ->wait(5000, "jQuery('.machine-name-value').text() === '{$bundleMachineName}'"); + + // Select our test bundle type. + $this->assertSession()->fieldExists('Handler'); + $this->assertSession()->optionExists('Handler', 'test'); + $page->selectFieldOption('Handler', 'test'); + $this->assertSession()->assertWaitOnAjaxRequest(); + + $page->pressButton('Save'); + + // Check whether the source field was correctly created. + $this->drupalGet("admin/structure/media/manage/{$bundleMachineName}/fields"); + + // Check 2nd column of first data row, to be machine name for field name. + $this->assertSession() + ->elementContains('xpath', '(//table[@id="field-overview"]//tr)[2]//td[2]', 'field_media_test'); + // Check 3rd column of first data row, to be correct field type. + $this->assertSession() + ->elementTextContains('xpath', '(//table[@id="field-overview"]//tr)[2]//td[3]', 'Text (plain)'); + + // Check that the source field is correctly assigned to media bundle. + $this->drupalGet("admin/structure/media/manage/{$bundleMachineName}"); + + $this->assertSession()->pageTextContains('Test handler field is used to store the essential information about the media item.'); + } + + /** + * Test creation of media bundle, reusing an existing source field. + */ + public function testBundleCreationReuseSourceField() { + // Create a new bundle, which should create a new field we can reuse. + $this->drupalGet('/admin/structure/media/add'); + $page = $this->getSession()->getPage(); + $page->fillField('label', 'Pastafazoul'); + $this->getSession() + ->wait(5000, "jQuery('.machine-name-value').text() === 'pastafazoul'"); + $page->selectFieldOption('Handler', 'test'); + $this->assertSession()->assertWaitOnAjaxRequest(); + $page->pressButton('Save'); + + $label = 'Bundle reusing Default Field'; + $bundleMachineName = str_replace(' ', '_', strtolower($label)); + + $this->drupalGet('admin/structure/media/add'); + $page = $this->getSession()->getPage(); + + // Fill in a label to the bundle. + $page->fillField('label', $label); + + // Wait for machine name generation. Default: waitUntilVisible(), does not + // work properly. + $this->getSession() + ->wait(5000, "jQuery('.machine-name-value').text() === '{$bundleMachineName}'"); + + // Select our test bundle type. + $this->assertSession()->fieldExists('Handler'); + $this->assertSession()->optionExists('Handler', 'test'); + $page->selectFieldOption('Handler', 'test'); + $this->assertSession()->assertWaitOnAjaxRequest(); + // Select the existing field for re-use. + $page->selectFieldOption('handler_configuration[source_field]', 'field_media_test'); + $page->pressButton('Save'); + + // Check that there are not fields created. + $this->drupalGet("admin/structure/media/manage/{$bundleMachineName}/fields"); + // The reused field should be present... + $this->assertSession()->pageTextContains('field_media_test'); + // ...not a new, unique one. + $this->assertSession()->pageTextNotContains('field_media_test_1'); + } + +} diff --git a/core/modules/media/tests/src/FunctionalJavascript/FileTest.php b/core/modules/media/tests/src/FunctionalJavascript/FileTest.php new file mode 100644 index 0000000..9ea399c --- /dev/null +++ b/core/modules/media/tests/src/FunctionalJavascript/FileTest.php @@ -0,0 +1,50 @@ +getSession(); + $page = $session->getPage(); + $assert_session = $this->assertSession(); + + // We rely on an automatic source field being created at this point. + // @see MediaTypeBase::getSourceFieldName(). + $source_field_name = 'field_media_file'; + $bundle_name = strtolower($this->randomMachineName(12)); + $provided_fields = ['mime', 'size']; + $this->createMediaTypeTest($bundle_name, 'file', $provided_fields); + \Drupal::service('entity_field.manager')->clearCachedFieldDefinitions(); + // Adjust the allowed extensions on the source field. + $this->drupalGet("admin/structure/media/manage/$bundle_name/fields/media.$bundle_name.$source_field_name"); + $page->fillField('settings[file_extensions]', 'txt'); + $page->pressButton('Save settings'); + // 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[' . $source_field_name . '_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('README.txt', $media->label()); + + } + +} diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaHandlerTestBase.php b/core/modules/media/tests/src/FunctionalJavascript/MediaHandlerTestBase.php new file mode 100644 index 0000000..911f339 --- /dev/null +++ b/core/modules/media/tests/src/FunctionalJavascript/MediaHandlerTestBase.php @@ -0,0 +1,96 @@ +removeComponent($field_name)->save(); + } + + /** + * Helper to test a generic type (bundle) creation. + * + * @param string $type_name + * The type machine name. + * @param string $type_id + * The bundle type ID. + * @param array $provided_fields + * (optional) An array of field machine names this type provides. + * + * @return \Drupal\media\MediaTypeInterface + * The type created. + */ + public function createMediaTypeTest($type_name, $type_id, array $provided_fields = []) { + $session = $this->getSession(); + $page = $session->getPage(); + $assert_session = $this->assertSession(); + + $this->drupalGet('admin/structure/media/add'); + $page->fillField('label', $type_name); + $this->assertJsCondition("jQuery('.machine-name-value').text() === '$type_name'"); + + // Make sure the type is available. + $assert_session->optionExists('handler', $type_id); + $page->selectFieldOption('handler', $type_id); + $assert_session->assertWaitOnAjaxRequest(); + + // 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 type. + $page->pressButton('Save'); + $assert_session->pageTextContains('The media type ' . $type_name . ' has been added.'); + $this->drupalGet('admin/structure/media'); + $assert_session->pageTextContains($type_name); + + // Bundle definitions are statically cached in the context of the test, we + // need to make sure we have updated information before proceeding with the + // actions on the UI. + \Drupal::service('entity_type.bundle.info')->clearCachedBundles(); + + return MediaType::load($type_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/tests/src/FunctionalJavascript/MediaJavascriptTestBase.php b/core/modules/media/tests/src/FunctionalJavascript/MediaJavascriptTestBase.php new file mode 100644 index 0000000..0e43fff --- /dev/null +++ b/core/modules/media/tests/src/FunctionalJavascript/MediaJavascriptTestBase.php @@ -0,0 +1,112 @@ +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/tests/src/FunctionalJavascript/MediaUiJavascriptTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaUiJavascriptTest.php new file mode 100644 index 0000000..c61ffe1 --- /dev/null +++ b/core/modules/media/tests/src/FunctionalJavascript/MediaUiJavascriptTest.php @@ -0,0 +1,179 @@ +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(); + + $this->drupalGet('admin/structure/media'); + $assert_session->statusCodeEquals(200); + $assert_session->pageTextContains('No media types available. Add media type.'); + $assert_session->linkExists('Add media type'); + + // 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); + $machine_name = strtolower($name); + $this->assertJsCondition("jQuery('.machine-name-value').html() == '$machine_name'"); + $page->selectFieldOption('handler', 'test'); + $this->assertJsCondition("jQuery('.form-item-handler-configuration-test-config-value').length > 0;"); + $page->fillField('description', $description); + $page->pressButton('Save'); + $assert_session->statusCodeEquals(200); + $assert_session->pageTextContains('The media type ' . $name . ' has been added.'); + $this->drupalGet('admin/structure/media'); + $assert_session->statusCodeEquals(200); + $assert_session->pageTextContains($name); + $assert_session->pageTextContains($description); + + /** @var \Drupal\media\MediaTypeInterface $bundle_storage */ + $bundle_storage = $this->container->get('entity_type.manager')->getStorage('media_type'); + $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('handler', 'test'); + $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 new revisions. Users with the Administer media permission will be able to override this option.'); + $assert_session->pageTextContains('Download thumbnails via a queue.'); + $assert_session->pageTextContains('Media will be automatically published when created.'); + $assert_session->pageTextContains('Media handlers can provide metadata fields such as title, caption, size information, credits, etc. Media 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('handler', 'test'); + $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('handler', 'test'); + $page->fillField('Test config value', 'This is new config value.'); + $page->selectFieldOption('field_map[field_1]', 'name'); + $page->checkField('options[new_revision]'); + $page->uncheckField('options[status]'); + $page->checkField('options[queue_thumbnail_downloads]'); + $page->pressButton('Save'); + $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('handler', 'test'); + $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\MediaTypeInterface $loaded_bundle */ + $loaded_bundle = $this->container->get('entity_type.manager') + ->getStorage('media_type') + ->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->getHandler()->getPluginId(), 'test'); + $this->assertEquals($loaded_bundle->getHandler()->getConfiguration()['test_config_value'], 'This is new config value.'); + $this->assertTrue($loaded_bundle->shouldCreateNewRevision()); + $this->assertTrue($loaded_bundle->getQueueThumbnailDownloadsStatus()); + $this->assertFalse($loaded_bundle->getStatus()); + $this->assertEquals($loaded_bundle->getFieldMap(), ['field_1' => 'name']); + + // We need to clear the statically cached field definitions to account for + // fields that have been created by API calls in this test, since they exist + // in a separate memory space from the web server. + $this->container->get('entity_field.manager')->clearCachedFieldDefinitions(); + + // Test that a media being created with default status to "FALSE" will be + // created unpublished. + /** @var \Drupal\media\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 type ' . $new_name . ' has been deleted.'); + + // Test bundle delete prevention when there is existing media. + $bundle2 = $this->drupalCreateMediaType(['auto_create_source_field' => TRUE]); + $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/tests/src/FunctionalJavascript/MediaViewsWizardTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaViewsWizardTest.php new file mode 100644 index 0000000..99515a2 --- /dev/null +++ b/core/modules/media/tests/src/FunctionalJavascript/MediaViewsWizardTest.php @@ -0,0 +1,88 @@ +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/tests/src/Kernel/BasicCreationTest.php b/core/modules/media/tests/src/Kernel/BasicCreationTest.php new file mode 100644 index 0000000..231bc64 --- /dev/null +++ b/core/modules/media/tests/src/Kernel/BasicCreationTest.php @@ -0,0 +1,203 @@ +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 = MediaType::create([ + 'id' => $id, + 'label' => $id, + 'handler' => 'test', + 'new_revision' => FALSE, + 'auto_create_source_field' => TRUE, + ]); + $this->testBundle->save(); + } + + /** + * Tests creating a media bundle programmatically. + */ + public function testMediaBundleCreation() { + $bundle_storage = $this->container->get('entity_type.manager')->getStorage('media_type'); + + $this->assertInstanceOf(MediaTypeInterface::class, MediaType::load($this->testBundle->id()), 'The new media type has not been correctly created in the database.'); + + // Test a media type created from default configuration. + $this->container->get('module_installer')->install(['media_test_type']); + $test_bundle = $bundle_storage->load('test'); + $this->assertInstanceOf(MediaTypeInterface::class, $test_bundle, 'The media type from default configuration has not been created in the database.'); + $this->assertEquals('Test type', $test_bundle->get('label'), 'Could not assure the correct type name.'); + $this->assertEquals('Test type.', $test_bundle->get('description'), 'Could not assure the correct type description.'); + $this->assertEquals('test', $test_bundle->get('handler'), 'Could not assure the correct handler.'); + // Source field is not set on the handler, but it should never be created + // automatically when a config is being imported. + $this->assertEquals(['source_field' => '', 'test_config_value' => 'Kakec'], $test_bundle->get('handler_configuration'), 'Could not assure the correct handler 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', + 'field_media_test' => 'Nation of sheep, ruled by wolves, owned by pigs.', + ]); + $media->save(); + + $this->assertNotInstanceOf(MediaInterface::class, Media::load(rand(1000, 9999)), 'Failed asserting a non-existent media.'); + + $this->assertInstanceOf(MediaInterface::class, Media::load($media->id()), 'The new media entity has not been created in the database.'); + $this->assertEquals($this->testBundle->id(), $media->bundle(), 'The media was not created with the correct type.'); + $this->assertEquals('Unnamed', $media->label(), 'The media was not created with the correct name.'); + $this->assertEquals('Nation of sheep, ruled by wolves, owned by pigs.', $media->bundle->entity->getHandler()->getSourceValue($media)->getValue(), 'Handler returns correct source value.'); + + // 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($this->testBundle->id(), $media->bundle(), 'The media was not created with correct type.'); + $this->assertEquals($expected_name, $media->label(), 'The media was not created with a default name.'); + } + + /** + * Tests creating and updating bundles programmatically. + */ + public function testProgrammaticBundleManipulation() { + // Creating a bundle programmatically without specifying a source field + // should create one automagically. + /** @var FieldConfig $field */ + $field = $this->testBundle->getHandler()->getSourceField($this->testBundle); + $this->assertInstanceOf(FieldConfig::class, $field); + $this->assertEquals('field_media_test', $field->getName()); + $this->assertFalse($field->isNew()); + + // Saving with a non-existent source field should create it if the related + // flag is set. + $this->testBundle->getHandler()->setConfiguration([ + 'source_field' => 'field_magick', + ]); + $this->testBundle->set('auto_create_source_field', TRUE); + $this->testBundle->save(); + $field = $this->testBundle->getHandler()->getSourceField($this->testBundle); + $this->assertInstanceOf(FieldConfig::class, $field); + $this->assertEquals('field_magick', $field->getName()); + $this->assertFalse($field->isNew()); + + // Trying to save without a source field should create a new, de-duped one. + $this->testBundle->getHandler()->setConfiguration([]); + $this->testBundle->set('auto_create_source_field', TRUE); + $this->testBundle->save(); + $field = $this->testBundle->getHandler()->getSourceField($this->testBundle); + $this->assertInstanceOf(FieldConfig::class, $field); + $this->assertEquals('field_media_test_1', $field->getName()); + $this->assertFalse($field->isNew()); + + // Trying to reuse an existing field should, well, reuse the existing field + // even if the auto create flag is set. + $this->testBundle->getHandler()->setConfiguration([ + 'source_field' => 'field_magick', + ]); + $this->testBundle->set('auto_create_source_field', TRUE); + $this->testBundle->save(); + $field = $this->testBundle->getHandler()->getSourceField($this->testBundle); + $this->assertInstanceOf(FieldConfig::class, $field); + $this->assertEquals('field_magick', $field->getName()); + $this->assertFalse($field->isNew()); + // No new de-duped fields should have been created. + $duplicates = FieldConfig::loadMultiple([ + 'media.' . $this->testBundle->id() . '.field_magick_1', + 'media.' . $this->testBundle->id() . '.field_media_generic_2', + ]); + $this->assertEmpty($duplicates); + + // If auto-create flag is not set fields shouldn't be created. + // ... if an imaginary field is set. + $this->testBundle->getHandler()->setConfiguration([ + 'source_field' => 'field_imaginary', + ]); + $this->testBundle->set('auto_create_source_field', FALSE); + $this->testBundle->save(); + $field = $this->container->get('entity_type.manager') + ->getStorage('field_config') + ->load('media.' . $this->testBundle->id() . '.field_imaginary'); + $field_storage = $this->container->get('entity_type.manager') + ->getStorage('field_storage_config') + ->load('media.field_imaginary'); + $this->assertNull($field, 'Field was not automatically created.'); + $this->assertNull($field_storage, 'Field was not automatically created.'); + + // ... if no field is set. + $this->testBundle->getHandler()->setConfiguration([]); + $this->testBundle->set('auto_create_source_field', FALSE); + $this->testBundle->save(); + $config = $this->testBundle->getHandler()->getConfiguration(); + $this->assertEquals('', $config['source_field'], 'Source field value was not automatically populated.'); + // Check what would the field be named like if it was created. + $proposed_field = $this->testBundle->getHandler()->getSourceField($this->testBundle); + $this->assertInstanceOf(FieldConfig::class, $proposed_field); + $this->assertEquals('field_media_test_2', $proposed_field->getName()); + $this->assertTrue($proposed_field->isNew()); + $field = $this->container->get('entity_type.manager') + ->getStorage('field_config') + ->load('media.' . $this->testBundle->id() . '.' . $proposed_field->getName()); + $field_storage = $this->container->get('entity_type.manager') + ->getStorage('field_storage_config') + ->load('media.' . $proposed_field->getName()); + $this->assertNull($field, 'Field was not automatically created.'); + $this->assertNull($field_storage, 'Field was not automatically created.'); + } + +} diff --git a/core/modules/media/tests/src/Kernel/TokensTest.php b/core/modules/media/tests/src/Kernel/TokensTest.php new file mode 100644 index 0000000..e5e510c --- /dev/null +++ b/core/modules/media/tests/src/Kernel/TokensTest.php @@ -0,0 +1,74 @@ +installEntitySchema('file'); + $this->installSchema('file', 'file_usage'); + $this->installEntitySchema('media'); + $this->installConfig(['language', 'datetime', 'field', 'system']); + } + + /** + * Tests some of the tokens provided by Media. + */ + public function testMediaEntityTokens() { + // Create a test media bundle. + $bundle_name = $this->randomMachineName(); + + MediaType::create([ + 'id' => $bundle_name, + 'label' => $bundle_name, + 'handler' => 'test', + 'handler_configuration' => ['test_config_value' => 'Kakec'], + '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 successful.'); + } + +} diff --git a/core/profiles/standard/config/optional/core.entity_form_display.media.file.default.yml b/core/profiles/standard/config/optional/core.entity_form_display.media.file.default.yml new file mode 100644 index 0000000..15de2ac --- /dev/null +++ b/core/profiles/standard/config/optional/core.entity_form_display.media.file.default.yml @@ -0,0 +1,44 @@ +langcode: en +status: true +dependencies: + config: + - field.field.media.file.field_media_file + - media.type.file + module: + - file +id: media.file.default +targetEntityType: media +bundle: file +mode: default +content: + created: + type: datetime_timestamp + weight: 10 + region: content + settings: { } + third_party_settings: { } + field_media_file: + settings: + progress_indicator: throbber + third_party_settings: { } + type: file_generic + weight: 26 + region: content + name: + type: string_textfield + weight: -5 + region: content + settings: + size: 60 + placeholder: '' + third_party_settings: { } + uid: + type: entity_reference_autocomplete + weight: 5 + settings: + match_operator: CONTAINS + size: 60 + placeholder: '' + region: content + third_party_settings: { } +hidden: { } diff --git a/core/profiles/standard/config/optional/core.entity_view_display.media.file.default.yml b/core/profiles/standard/config/optional/core.entity_view_display.media.file.default.yml new file mode 100644 index 0000000..697f117 --- /dev/null +++ b/core/profiles/standard/config/optional/core.entity_view_display.media.file.default.yml @@ -0,0 +1,50 @@ +langcode: en +status: true +dependencies: + config: + - field.field.media.file.field_media_file + - image.style.thumbnail + - media.type.file + module: + - file + - image + - user +id: media.file.default +targetEntityType: media +bundle: file +mode: default +content: + created: + label: hidden + type: timestamp + weight: 0 + region: content + settings: + date_format: medium + custom_date_format: '' + timezone: '' + third_party_settings: { } + field_media_file: + label: above + settings: { } + third_party_settings: { } + type: file_default + weight: 6 + region: content + thumbnail: + type: image + weight: 5 + label: hidden + settings: + image_style: thumbnail + image_link: '' + region: content + third_party_settings: { } + uid: + label: hidden + type: author + weight: 0 + region: content + settings: { } + third_party_settings: { } +hidden: { } diff --git a/core/profiles/standard/config/optional/field.field.media.file.field_media_file.yml b/core/profiles/standard/config/optional/field.field.media.file.field_media_file.yml new file mode 100644 index 0000000..740d5df --- /dev/null +++ b/core/profiles/standard/config/optional/field.field.media.file.field_media_file.yml @@ -0,0 +1,29 @@ +langcode: en +status: true +dependencies: + config: + - field.storage.media.field_media_file + - media.type.file + module: + - file + enforced: + module: + - media +id: media.file.field_media_file +field_name: field_media_file +entity_type: media +bundle: file +label: File +description: '' +required: true +translatable: true +default_value: { } +default_value_callback: '' +settings: + file_directory: '[date:custom:Y]-[date:custom:m]' + file_extensions: 'txt pdf' + max_filesize: '' + handler: 'default:file' + handler_settings: { } + description_field: false +field_type: file diff --git a/core/profiles/standard/config/optional/field.storage.media.field_media_file.yml b/core/profiles/standard/config/optional/field.storage.media.field_media_file.yml new file mode 100644 index 0000000..a830fa8 --- /dev/null +++ b/core/profiles/standard/config/optional/field.storage.media.field_media_file.yml @@ -0,0 +1,25 @@ +langcode: en +status: true +dependencies: + module: + - file + - media + enforced: + module: + - media +id: media.field_media_file +field_name: field_media_file +entity_type: media +type: file +settings: + uri_scheme: public + target_type: file + display_field: false + display_default: false +module: file +locked: false +cardinality: 1 +translatable: true +indexes: { } +persist_with_no_fields: false +custom_storage: false diff --git a/core/profiles/standard/config/optional/media.type.file.yml b/core/profiles/standard/config/optional/media.type.file.yml new file mode 100644 index 0000000..ec38e0f --- /dev/null +++ b/core/profiles/standard/config/optional/media.type.file.yml @@ -0,0 +1,14 @@ +langcode: en +status: true +dependencies: + module: + - media +id: file +label: File +description: 'Use the "File" media type for uploading local files.' +handler: file +queue_thumbnail_downloads: false +new_revision: false +handler_configuration: + source_field: field_media_file +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 0000000..831a3e6 --- /dev/null +++ b/core/themes/classy/templates/content/media.html.twig @@ -0,0 +1,27 @@ +{# +/** + * @file + * Theme override to display a media. + * + * Available variables: + * - name: Name of the media. + * - content: Media content. + * + * @see template_preprocess_media() + * + * @ingroup themeable + */ +#} +{% + set classes = [ + 'media', + 'media--type-' ~ media.bundle()|clean_class, + not media.isPublished() ? 'media--unpublished', + view_mode ? 'media--view-mode-' ~ view_mode.id()|clean_class, + ] +%} + + {% 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 0000000..2bdd59e --- /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 %} +