diff --git a/core/composer.json b/core/composer.json index c3600ab1a6..31aeb4c3a2 100644 --- a/core/composer.json +++ b/core/composer.json @@ -114,6 +114,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/core.entity_view_mode.media.full.yml b/core/modules/media/config/install/core.entity_view_mode.media.full.yml new file mode 100644 index 0000000000..dfdbb3a7d6 --- /dev/null +++ b/core/modules/media/config/install/core.entity_view_mode.media.full.yml @@ -0,0 +1,9 @@ +langcode: en +status: false +dependencies: + module: + - media +id: media.full +label: 'Full content' +targetEntityType: media +cache: true 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 0000000000..853e575c67 --- /dev/null +++ b/core/modules/media/config/install/media.settings.yml @@ -0,0 +1 @@ +icon_base_uri: '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 0000000000..62af35ea5d --- /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 0000000000..95e173d6f7 --- /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 0000000000..c4d098f4bc --- /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 0000000000..4189e4e743 --- /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/optional/views.view.media.yml b/core/modules/media/config/optional/views.view.media.yml new file mode 100644 index 0000000000..745420cd30 --- /dev/null +++ b/core/modules/media/config/optional/views.view.media.yml @@ -0,0 +1,854 @@ +langcode: en +status: true +dependencies: + module: + - image + - media + - user +id: media +label: Media +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: + access: + type: perm + options: + perm: 'access media overview' + cache: + type: tag + options: { } + query: + type: views_query + options: + disable_sql_rewrite: false + distinct: false + replica: false + query_comment: '' + query_tags: { } + exposed_form: + type: basic + options: + submit_button: Apply + reset_button: false + reset_button_label: Reset + exposed_sorts_label: 'Sort by' + expose_sort_order: true + sort_asc_label: Asc + sort_desc_label: Desc + pager: + type: full + options: + items_per_page: 50 + offset: 0 + id: 0 + total_pages: null + expose: + items_per_page: false + items_per_page_label: 'Items per page' + items_per_page_options: '5, 10, 25, 50' + items_per_page_options_all: false + items_per_page_options_all_label: '- All -' + offset: false + offset_label: Offset + tags: + previous: '‹ Previous' + next: 'Next ›' + first: '« First' + last: 'Last »' + quantity: 9 + style: + type: table + options: + grouping: { } + row_class: '' + default_row_class: true + override: true + sticky: false + caption: '' + summary: '' + description: '' + columns: + name: name + bundle: bundle + changed: changed + uid: uid + status: status + thumbnail__target_id: thumbnail__target_id + info: + name: + sortable: true + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + bundle: + sortable: true + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + changed: + sortable: true + default_sort_order: desc + align: '' + separator: '' + empty_column: false + responsive: '' + uid: + sortable: false + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + status: + sortable: true + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + thumbnail__target_id: + sortable: false + default_sort_order: asc + align: '' + separator: '' + empty_column: false + responsive: '' + default: changed + empty_table: false + row: + type: fields + fields: + media_bulk_form: + id: media_bulk_form + table: media + field: media_bulk_form + relationship: none + group_type: group + admin_label: '' + label: '' + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: false + 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: Action + include_exclude: exclude + selected_actions: { } + entity_type: media + plugin_id: media_bulk_form + thumbnail__target_id: + id: thumbnail__target_id + table: media_field_data + field: thumbnail__target_id + relationship: none + group_type: group + admin_label: '' + label: Thumbnail + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: 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: target_id + type: image + settings: + image_style: thumbnail + image_link: '' + group_column: '' + 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: thumbnail + plugin_id: field + name: + id: name + table: media_field_data + field: name + entity_type: media + entity_field: media + alter: + alter_text: false + make_link: false + absolute: false + trim: false + word_boundary: false + ellipsis: false + strip_tags: false + html: false + hide_empty: false + empty_zero: false + settings: + link_to_entity: true + 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 + bundle: + id: bundle + table: media_field_data + field: bundle + relationship: none + group_type: group + admin_label: '' + label: Provider + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: 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: target_id + type: entity_reference_label + settings: + link: false + group_column: target_id + 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: bundle + plugin_id: field + uid: + id: uid + table: media_field_data + field: uid + relationship: none + group_type: group + admin_label: '' + label: Author + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: 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: target_id + type: entity_reference_label + settings: + link: true + group_column: target_id + 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: uid + plugin_id: field + status: + id: status + table: media_field_data + field: status + relationship: none + group_type: group + admin_label: '' + label: Status + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: 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 + changed: + id: changed + table: media_field_data + field: changed + relationship: none + group_type: group + admin_label: '' + label: Updated + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: 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: timestamp + settings: + date_format: short + custom_date_format: '' + timezone: '' + 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: changed + plugin_id: field + operations: + id: operations + table: media + field: operations + relationship: none + group_type: group + admin_label: '' + label: Operations + exclude: false + alter: + alter_text: false + text: '' + make_link: false + path: '' + absolute: false + external: false + replace_spaces: false + path_case: none + trim_whitespace: false + alt: '' + rel: '' + link_class: '' + prefix: '' + suffix: '' + target: '' + nl2br: false + max_length: 0 + word_boundary: true + ellipsis: true + more_link: false + more_link_text: '' + more_link_path: '' + strip_tags: false + trim: false + preserve_tags: '' + html: 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 + destination: true + entity_type: media + plugin_id: entity_operations + filters: + status: + id: status + table: media_field_data + field: status + relationship: none + group_type: group + admin_label: '' + operator: '=' + value: '1' + group: 1 + exposed: true + expose: + operator_id: '' + label: 'True' + description: null + use_operator: false + operator: status_op + identifier: status + required: true + remember: false + multiple: false + remember_roles: + authenticated: authenticated + is_grouped: true + group_info: + label: 'Publishing status' + description: '' + identifier: status + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: + 1: + title: Published + operator: '=' + value: '1' + 2: + title: Unpublished + operator: '=' + value: '0' + plugin_id: boolean + entity_type: media + entity_field: status + bundle: + id: bundle + table: media_field_data + field: bundle + relationship: none + group_type: group + admin_label: '' + operator: in + value: { } + group: 1 + exposed: true + expose: + operator_id: bundle_op + label: Provider + description: '' + use_operator: false + operator: bundle_op + identifier: provider + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + reduce: false + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + entity_type: media + entity_field: bundle + plugin_id: bundle + name: + id: name + table: media_field_data + field: name + relationship: none + group_type: group + admin_label: '' + operator: contains + value: '' + group: 1 + exposed: true + expose: + operator_id: name_op + label: 'Media name' + description: '' + use_operator: false + operator: name_op + identifier: name + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + entity_type: media + entity_field: name + plugin_id: string + langcode: + id: langcode + table: media_field_data + field: langcode + relationship: none + group_type: group + admin_label: '' + operator: in + value: { } + group: 1 + exposed: true + expose: + operator_id: langcode_op + label: Language + description: '' + use_operator: false + operator: langcode_op + identifier: langcode + required: false + remember: false + multiple: false + remember_roles: + authenticated: authenticated + anonymous: '0' + administrator: '0' + reduce: false + is_grouped: false + group_info: + label: '' + description: '' + identifier: '' + optional: true + widget: select + multiple: false + remember: false + default_group: All + default_group_multiple: { } + group_items: { } + entity_type: media + entity_field: langcode + plugin_id: language + sorts: + created: + id: created + table: media_field_data + field: created + order: DESC + entity_type: media + entity_field: created + plugin_id: date + relationship: none + group_type: group + admin_label: '' + exposed: false + expose: + label: '' + granularity: second + title: Media + header: { } + footer: { } + empty: + area_text_custom: + id: area_text_custom + table: views + field: area_text_custom + relationship: none + group_type: group + admin_label: '' + empty: true + tokenize: false + content: 'No content available.' + plugin_id: text_custom + relationships: { } + arguments: { } + display_extenders: { } + cache_metadata: + max-age: 0 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - url.query_args + - user.permissions + tags: { } + media_page_list: + display_plugin: page + id: media_page_list + display_title: Media + position: 1 + display_options: + display_extenders: { } + path: admin/content/media + menu: + type: tab + title: Media + description: '' + expanded: false + parent: '' + weight: 0 + context: '0' + menu_name: main + display_description: '' + cache_metadata: + max-age: 0 + contexts: + - 'languages:language_content' + - 'languages:language_interface' + - url + - url.query_args + - user.permissions + tags: { } 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 0000000000..4783f6d0ca --- /dev/null +++ b/core/modules/media/config/schema/media.schema.yml @@ -0,0 +1,68 @@ +media.settings: + type: config_object + label: 'Media settings' + mapping: + icon_base_uri: + type: string + label: 'Full URI to a folder where the 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' + source: + type: string + label: 'Source' + source_configuration: + type: media.source.[%parent.source] + queue_thumbnail_downloads: + type: boolean + label: 'Whether the thumbnail downloads should be queued' + new_revision: + type: boolean + label: 'Whether a new revision should be created by default' + field_map: + type: sequence + label: 'Field map' + sequence: + type: string + +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.source.*: + type: mapping + label: 'Media source settings' + +media.source.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 0000000000..0c7371d51e --- /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 0000000000..2050a78a4f --- /dev/null +++ b/core/modules/media/images/icons/generic.png @@ -0,0 +1,3 @@ +PNG + + IHDRYfIDATx@@}7J0@YwwwwwwU9fM/, 4hРA4hРA 4hРA 4hРA Z,+h]~˽YX/6@G5$:deDT4z 󦤇Eh; mj` _;> ,E]%e{EH\$Хb]Zt77؇թEs3d K-Z jkULR1ךgoٱ}]SB(>N-mwRA.=!sm4O"Dه=uckw̕ӎI--BtChР5Nvze]{JCo6Og$>tq.ޔ43b\h4hР[)In :5O,s.$z z ]k6t6d35Z7~zӖ]Klо< !A 4hРA 4hРA 4hРA 4M+FEIENDB` \ 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 0000000000..c93eac6644 --- /dev/null +++ b/core/modules/media/js/media_form.js @@ -0,0 +1,40 @@ +/** + * @file + * Defines Javascript behaviors for the media form. + */ + +(function ($, Drupal) { + + 'use strict'; + + /** + * Behaviors for summaries 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.mediaFormSummaries = { + 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 0000000000..0c1d906e01 --- /dev/null +++ b/core/modules/media/js/media_type_form.js @@ -0,0 +1,46 @@ +/** + * @file + * Defines JavaScript behaviors 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.mediaTypeFormSummaries = { + 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 0000000000..8de1c64575 --- /dev/null +++ b/core/modules/media/media.api.php @@ -0,0 +1,25 @@ +get('icon_base_uri'); + file_prepare_directory($destination, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS); + + $files = file_scan_directory($source, '/.*\.(svg|png|jpg|jpeg|gif)$/'); + foreach ($files as $file) { + file_unmanaged_copy($file->uri, $destination, FILE_EXISTS_ERROR); + } +} + +/** + * Implements hook_requirements(). + */ +function media_requirements($phase) { + $requirements = []; + if ($phase == 'install') { + $destination = 'public://media-icons/generic'; + file_prepare_directory($destination, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS); + $is_writable = is_writable($destination); + $is_directory = is_dir($destination); + if (!$is_writable || !$is_directory) { + if (!$is_directory) { + $error = t('The directory %directory does not exist.', ['%directory' => $destination]); + } + else { + $error = t('The directory %directory is not writable.', ['%directory' => $destination]); + } + $description = t('An automated attempt to create this directory failed, possibly due to a permissions problem. To proceed with the installation, either create the directory and modify its permissions manually or ensure that the installer has the permissions to create it automatically. For more information, see INSTALL.txt or the online handbook.', [':handbook_url' => 'https://www.drupal.org/server-permissions']); + if (!empty($error)) { + $description = $error . ' ' . $description; + $requirements['media']['description'] = $description; + $requirements['media']['severity'] = REQUIREMENT_ERROR; + } + } + } + return $requirements; +} diff --git a/core/modules/media/media.libraries.yml b/core/modules/media/media.libraries.yml new file mode 100644 index 0000000000..eecaf8ed1f --- /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 0000000000..a056d35c48 --- /dev/null +++ b/core/modules/media/media.links.action.yml @@ -0,0 +1,12 @@ +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 + appears_on: + - view.media.media_page_list diff --git a/core/modules/media/media.links.contextual.yml b/core/modules/media/media.links.contextual.yml new file mode 100644 index 0000000000..1945ef5001 --- /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 0000000000..33dd67978c --- /dev/null +++ b/core/modules/media/media.links.menu.yml @@ -0,0 +1,6 @@ +entity.media_type.collection: + title: 'Media types' + parent: system.admin_structure + description: 'Manage media types.' + route_name: entity.media_type.collection + diff --git a/core/modules/media/media.links.task.yml b/core/modules/media/media.links.task.yml new file mode 100644 index 0000000000..c7a669c2c1 --- /dev/null +++ b/core/modules/media/media.links.task.yml @@ -0,0 +1,25 @@ +entity.media.canonical: + title: View + route_name: entity.media.canonical + base_route: entity.media.canonical + +entity.media.edit_form: + title: Edit + route_name: entity.media.edit_form + base_route: entity.media.canonical + +entity.media.delete_form: + title: Delete + route_name: entity.media.delete_form + base_route: entity.media.canonical + 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 0000000000..eeb2ac168a --- /dev/null +++ b/core/modules/media/media.module @@ -0,0 +1,116 @@ +' . 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 source (such as the image source) 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', + ], + ]; +} + +/** + * Implements hook_entity_operation_alter(). + * + * Fix broken operations array in field UI for entities with restricted access. + * + * @todo This hook can be removed when issue #2836384 is done. + * @see https://www.drupal.org/node/2836384 + */ +function media_entity_operation_alter(array &$operations, EntityInterface $entity) { + if ($entity instanceof FieldConfigInterface && $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->getSource()->getConfiguration()['source_field']) { + unset($operations['delete']); + } + } +} + +/** + * Implements hook_entity_access(). + */ +function media_entity_access(EntityInterface $entity, $operation, AccountInterface $account) { + if ($operation === 'delete' && $entity instanceof FieldConfigInterface && $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->getSource()->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; +} + +/** + * Prepares variables for media templates. + * + * Default template: media.html.twig. + * + * @param array $variables + * An associative array containing: + * - elements: An array of elements to display in view mode. + * - media: The media item. + * - name: The label for the media item. + * - view_mode: View mode; e.g., 'full', 'teaser', etc. + */ +function template_preprocess_media(array &$variables) { + $variables['media'] = $variables['elements']['#media']; + $variables['view_mode'] = $variables['elements']['#view_mode']; + $variables['name'] = $variables['media']->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.permissions.yml b/core/modules/media/media.permissions.yml new file mode 100644 index 0000000000..530365e1f4 --- /dev/null +++ b/core/modules/media/media.permissions.yml @@ -0,0 +1,25 @@ +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' + +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 0000000000..67ea1fb81c --- /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\MediaDeleteMultipleConfirmForm' + requirements: + _permission: 'administer media+delete any media' diff --git a/core/modules/media/media.services.yml b/core/modules/media/media.services.yml new file mode 100644 index 0000000000..a286b528d9 --- /dev/null +++ b/core/modules/media/media.services.yml @@ -0,0 +1,4 @@ +services: + plugin.manager.media.source: + class: Drupal\media\MediaSourceManager + parent: default_plugin_manager diff --git a/core/modules/media/media.tokens.inc b/core/modules/media/media.tokens.inc new file mode 100644 index 0000000000..e528a9d17a --- /dev/null +++ b/core/modules/media/media.tokens.inc @@ -0,0 +1,175 @@ + 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 item'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 item is written in.'), + ]; + $media['name'] = [ + 'name' => t('Name'), + 'description' => t('The name of this media item.'), + ]; + $media['author'] = [ + 'name' => t('Author'), + 'type' => 'user', + ]; + $media['url'] = [ + 'name' => t('URL'), + 'description' => t('The URL of the media item.'), + ]; + $media['edit-url'] = [ + 'name' => t('Edit URL'), + 'description' => t("The URL of the media item'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 item 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(), $date_format->id(), '', NULL, $langcode); + break; + + case 'changed': + $date_format = DateFormat::load('medium'); + $bubbleable_metadata->addCacheableDependency($date_format); + $replacements[$original] = \Drupal::service('date.formatter')->format($media->getChangedTime(), $date_format->id(), '', 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/MediaSource.php b/core/modules/media/src/Annotation/MediaSource.php new file mode 100644 index 0000000000..fe1735d909 --- /dev/null +++ b/core/modules/media/src/Annotation/MediaSource.php @@ -0,0 +1,108 @@ +get('created')->value; + } + + /** + * {@inheritdoc} + */ + public function setCreatedTime($timestamp) { + return $this->set('created', $timestamp); + } + + /** + * {@inheritdoc} + */ + public function getOwner() { + return $this->get('uid')->entity; + } + + /** + * {@inheritdoc} + */ + public function setOwner(UserInterface $account) { + return $this->set('uid', $account->id()); + } + + /** + * {@inheritdoc} + */ + public function getOwnerId() { + return $this->get('uid')->target_id; + } + + /** + * {@inheritdoc} + */ + public function setOwnerId($uid) { + $this->set('uid', $uid); + } + + /** + * {@inheritdoc} + */ + public function getSource() { + return $this->bundle->entity->getSource(); + } + + /** + * Update the thumbnail for the media item. + * + * @param bool $from_queue + * Specifies whether the thumbnail update is triggered from the queue. + * + * @return \Drupal\media\MediaInterface + * The updated media item. + */ + protected function updateThumbnail($from_queue = FALSE) { + $file_storage = \Drupal::service('entity_type.manager')->getStorage('file'); + $thumbnail_uri = $this->getThumbnailUri($from_queue); + $existing = $file_storage->getQuery() + ->condition('uri', $thumbnail_uri) + ->execute(); + + if ($existing) { + $this->thumbnail->target_id = reset($existing); + } + else { + /** @var \Drupal\file\FileInterface $file */ + $file = $file_storage->create(['uri' => $thumbnail_uri]); + if ($owner = $this->getOwner()) { + $file->setOwner($owner); + } + $file->setPermanent(); + $file->save(); + $this->thumbnail->target_id = $file->id(); + } + + // Set the thumbnail alt. + $media_source = $this->getSource(); + $plugin_definition = $media_source->getPluginDefinition(); + if (!empty($plugin_definition['thumbnail_alt_metadata_attribute'])) { + $this->thumbnail->alt = $media_source->getMetadata($this, $plugin_definition['thumbnail_alt_metadata_attribute']); + } + else { + $this->thumbnail->alt = $this->t('Thumbnail', [], ['langcode' => $this->langcode->value]); + } + + // Set the thumbnail title. + if (!empty($plugin_definition['thumbnail_title_metadata_attribute'])) { + $this->thumbnail->title = $media_source->getMetadata($this, $plugin_definition['thumbnail_title_metadata_attribute']); + } + else { + $this->thumbnail->title = $this->label(); + } + + return $this; + } + + /** + * Updates the queued thumbnail for the media item. + * + * @return \Drupal\media\MediaInterface + * The updated media item. + * + * @internal + * + * @todo If the need arises in contrib, consider making this a public API, + * by adding an interface that extends MediaInterface. + */ + public function updateQueuedThumbnail() { + $this->updateThumbnail(TRUE); + return $this; + } + + /** + * Gets a file URI for a media item. + * + * If thumbnail fetching should be queued then temporary use default + * thumbnail for new files or temporary keep existing thumbnail for + * updates. + * Immediately fetch a new thumbnail from the media source otherwise. + * + * @param bool $from_queue + * Specifies whether the thumbnail is being fetched from the queue. + * + * @return \Drupal\media\MediaInterface + * The file URI for the thumbnail of the media item. + */ + protected function getThumbnailUri($from_queue) { + $thumbnails_queued = $this->bundle->entity->thumbnailDownloadsAreQueued(); + if ($thumbnails_queued && $this->isNew()) { + $default_thumbnail_filename = $this->getSource()->getPluginDefinition()['default_thumbnail_filename']; + $thumbnail_uri = \Drupal::service('config.factory')->get('media.settings')->get('icon_base_uri') . '/' . $default_thumbnail_filename; + } + elseif ($thumbnails_queued && !$from_queue) { + $thumbnail_uri = $this->get('thumbnail')->entity->getFileUri(); + } + else { + $thumbnail_uri = $this->getSource()->getMetadata($this, $this->getSource()->getPluginDefinition()['thumbnail_uri_metadata_attribute']); + } + + return $thumbnail_uri; + } + + /** + * Determines if the source field value has changed. + * + * @return bool + * TRUE if the source field value changed, FALSE otherwise. + */ + protected function sourceFieldChanged() { + $source_field_name = $this->getSource()->getConfiguration()['source_field']; + $current_value = $this->get($source_field_name)->getValue(); + return (isset($this->original) && $current_value != $this->original->get($source_field_name)->getValue()); + } + + /** + * Determines if the thumbnail should be updated for a media item. + * + * @param bool $is_new + * Specifies whether the media item is new. + * + * @return bool + * TRUE if the thumbnail should be updated, FALSE otherwise. + */ + protected function shouldUpdateThumbnail($is_new = FALSE) { + // Update thumbnail if we don't have a thumbnail yet or when the source + // field value changes. + return !$this->get('thumbnail')->entity || $is_new || $this->sourceFieldChanged(); + } + + /** + * {@inheritdoc} + */ + public function preSave(EntityStorageInterface $storage) { + // If the source plugin defines any constraints we enforce the validation. + $media_source = $this->getSource(); + if ($media_source instanceof MediaSourceEntityConstraintsInterface || $media_source instanceof MediaSourceFieldConstraintsInterface) { + $this->setValidationRequired(TRUE); + } + + parent::preSave($storage); + + foreach ($this->translations as $langcode => $data) { + if ($this->hasTranslation($langcode)) { + $translation = $this->getTranslation($langcode); + // Try to set fields provided by the media source and mapped in + // media type config. + foreach ($translation->bundle->entity->getFieldMap() as $metadata_attribute_name => $entity_field_name) { + // Only save value in entity field if empty. Do not overwrite existing + // data. + if ($translation->hasField($entity_field_name) && ($translation->get($entity_field_name)->isEmpty() || $translation->sourceFieldChanged())) { + $translation->set($entity_field_name, $media_source->getMetadata($translation, $metadata_attribute_name)); + } + } + + // Try to set a default name for this media item if no label is + // provided. + if (!$translation->label()) { + $translation->set('name', $media_source->getMetadata($translation, $media_source->getPluginDefinition()['default_name_metadata_attribute'])); + } + + // Set thumbnail. + if ($translation->shouldUpdateThumbnail()) { + $translation->updateThumbnail(); + } + } + } + } + + /** + * {@inheritdoc} + */ + public function postSave(EntityStorageInterface $storage, $update = TRUE) { + parent::postSave($storage, $update); + $is_new = !$update; + foreach ($this->translations as $langcode => $data) { + if ($this->hasTranslation($langcode)) { + $translation = $this->getTranslation($langcode); + if ($translation->bundle->entity->thumbnailDownloadsAreQueued() && $translation->shouldUpdateThumbnail($is_new)) { + \Drupal::queue('media_entity_thumbnail')->createItem(['id' => $translation->id()]); + } + } + } + } + + /** + * {@inheritdoc} + */ + public function preSaveRevision(EntityStorageInterface $storage, \stdClass $record) { + parent::preSaveRevision($storage, $record); + + $is_new_revision = $this->isNewRevision(); + if (!$is_new_revision && isset($this->original) && empty($record->revision_log_message)) { + // If we are updating an existing media item without adding a + // new revision, we need to make sure $entity->revision_log_message 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_message = $this->original->revision_log_message->value; + } + + if ($is_new_revision) { + $record->revision_created = self::getRequestTime(); + } + } + + /** + * {@inheritdoc} + */ + public function validate() { + $media_source = $this->getSource(); + + if ($media_source instanceof MediaSourceEntityConstraintsInterface) { + $entity_constraints = $media_source->getEntityConstraints(); + $this->getTypedData()->getDataDefinition()->setConstraints($entity_constraints); + } + + if ($media_source instanceof MediaSourceFieldConstraintsInterface) { + $source_field_name = $media_source->getConfiguration()['source_field']; + $source_field_constraints = $media_source->getSourceFieldConstraints(); + $this->get($source_field_name)->getDataDefinition()->setConstraints($source_field_constraints); + } + + 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 item.')) + ->setRevisionable(TRUE) + ->setTranslatable(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 user ID of the author.')) + ->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) + ->setDefaultValueCallback(static::class . '::getRequestTime') + ->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); + + 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 static function getRequestTime() { + return \Drupal::time()->getRequestTime(); + } + +} diff --git a/core/modules/media/src/Entity/MediaType.php b/core/modules/media/src/Entity/MediaType.php new file mode 100644 index 0000000000..4682f0e6b0 --- /dev/null +++ b/core/modules/media/src/Entity/MediaType.php @@ -0,0 +1,225 @@ + $this->sourcePluginCollection(), + ]; + } + + /** + * {@inheritdoc} + */ + public function getDescription() { + return $this->description; + } + + /** + * {@inheritdoc} + */ + public function setDescription($description) { + return $this->set('description', $description); + } + + /** + * {@inheritdoc} + */ + public function thumbnailDownloadsAreQueued() { + return $this->queue_thumbnail_downloads; + } + + /** + * {@inheritdoc} + */ + public function setQueueThumbnailDownloadsStatus($queue_thumbnail_downloads) { + return $this->set('queue_thumbnail_downloads', $queue_thumbnail_downloads); + } + + /** + * {@inheritdoc} + */ + public function getSource() { + return $this->sourcePluginCollection()->get($this->source); + } + + /** + * Returns media source lazy plugin collection. + * + * @return \Drupal\Core\Plugin\DefaultSingleLazyPluginCollection|null + * The tag plugin collection or NULL if the plugin ID was not set yet. + */ + protected function sourcePluginCollection() { + if (!$this->sourcePluginCollection && $this->source) { + $this->sourcePluginCollection = new DefaultSingleLazyPluginCollection(\Drupal::service('plugin.manager.media.source'), $this->source, $this->source_configuration); + } + return $this->sourcePluginCollection; + } + + /** + * {@inheritdoc} + */ + public function getStatus() { + return $this->status; + } + + /** + * {@inheritdoc} + */ + public function shouldCreateNewRevision() { + return $this->new_revision; + } + + /** + * {@inheritdoc} + */ + public function setNewRevision($new_revision) { + return $this->set('new_revision', $new_revision); + } + + /** + * {@inheritdoc} + */ + public function getFieldMap() { + return $this->field_map; + } + + /** + * {@inheritdoc} + */ + public function setFieldMap(array $map) { + return $this->set('field_map', $map); + } + +} diff --git a/core/modules/media/src/Form/MediaDeleteMultipleConfirmForm.php b/core/modules/media/src/Form/MediaDeleteMultipleConfirmForm.php new file mode 100644 index 0000000000..466abb26a3 --- /dev/null +++ b/core/modules/media/src/Form/MediaDeleteMultipleConfirmForm.php @@ -0,0 +1,207 @@ +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->mediaItems), 'Are you sure you want to delete this item?', 'Are you sure you want to delete these items?'); + } + + /** + * {@inheritdoc} + */ + public function getCancelUrl() { + // @todo Change to media library when #2834729 is done. + // https://www.drupal.org/node/2834729. + return new Url('system.admin_content'); + } + + /** + * {@inheritdoc} + */ + public function getConfirmText() { + return $this->t('Delete'); + } + + /** + * {@inheritdoc} + * + * @todo Change to trait or base class when #2843395 is done. + * @see https://www.drupal.org/node/2843395 + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $this->mediaItems = $this->tempStoreFactory->get('media_multiple_delete_confirm')->get($this->currentUser()->id()); + if (empty($this->mediaItems)) { + return new RedirectResponse($this->getCancelUrl()->setAbsolute()->toString()); + } + /** @var \Drupal\media\MediaInterface[] $entities */ + $entities = $this->storage->loadMultiple(array_keys($this->mediaItems)); + + $items = []; + foreach ($this->mediaItems 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} + * + * @todo Change to trait or base class when #2843395 is done. + * @see https://www.drupal.org/node/2843395 + */ + public function submitForm(array &$form, FormStateInterface $form_state) { + if ($form_state->getValue('confirm') && !empty($this->mediaItems)) { + $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->mediaItems)); + + foreach ($this->mediaItems 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 items.', ['@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 item.', 'Deleted @count media items.')); + } + + $this->tempStoreFactory->get('media_multiple_delete_confirm')->delete(\Drupal::currentUser()->id()); + } + + // @todo Change to media library when #2834729 is done. + // https://www.drupal.org/node/2834729. + $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 0000000000..a181858b86 --- /dev/null +++ b/core/modules/media/src/Form/MediaTypeDeleteConfirmForm.php @@ -0,0 +1,68 @@ +entityTypeManager = $entity_type_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('entity_type.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function buildForm(array $form, FormStateInterface $form_state) { + $num_entities = $this->entityTypeManager->getStorage('media')->getQuery() + ->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 media item on your site. You can not remove this media type until you have removed all of the %type media items.', + '%type is used by @count media items on your site. You can not remove this media type until you have removed all of the %type media items.', + ['%type' => $this->entity->label()]), + ], + ]; + + return $form; + } + + return parent::buildForm($form, $form_state); + } + +} diff --git a/core/modules/media/src/MediaAccessControlHandler.php b/core/modules/media/src/MediaAccessControlHandler.php new file mode 100644 index 0000000000..00b7d1c32d --- /dev/null +++ b/core/modules/media/src/MediaAccessControlHandler.php @@ -0,0 +1,60 @@ +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': + if ($account->hasPermission('update any media')) { + return AccessResult::allowed()->cachePerPermissions(); + } + return AccessResult::allowedIf($account->hasPermission('update media') && $is_owner) + ->cachePerPermissions() + ->cachePerUser() + ->addCacheableDependency($entity); + + case 'delete': + if ($account->hasPermission('delete any media')) { + return AccessResult::allowed()->cachePerPermissions(); + } + return AccessResult::allowedIf($account->hasPermission('delete media') && $is_owner) + ->cachePerPermissions() + ->cachePerUser() + ->addCacheableDependency($entity); + + default: + 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 0000000000..1e91e9f627 --- /dev/null +++ b/core/modules/media/src/MediaForm.php @@ -0,0 +1,157 @@ +entity->bundle->entity; + + if ($this->operation === 'edit') { + $form['#title'] = $this->t('Edit %type_label @label', [ + '%type_label' => $media_type->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()) { + $element['publish']['#button_type'] = 'primary'; + } + // Otherwise, the 'unpublish' button is primary and should come first. + else { + $element['unpublish']['#button_type'] = 'primary'; + $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/MediaInterface.php b/core/modules/media/src/MediaInterface.php new file mode 100644 index 0000000000..70644418ed --- /dev/null +++ b/core/modules/media/src/MediaInterface.php @@ -0,0 +1,49 @@ +entityTypeManager = $entity_type_manager; + $this->entityFieldManager = $entity_field_manager; + $this->fieldTypeManager = $field_type_manager; + $this->configFactory = $config_factory; + + // Add the default configuration of the media source to the plugin. + $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('plugin.manager.field.field_type'), + $container->get('config.factory') + ); + } + + /** + * {@inheritdoc} + */ + public function setConfiguration(array $configuration) { + $this->configuration = NestedArray::mergeDeep( + $this->defaultConfiguration(), + $configuration + ); + } + + /** + * {@inheritdoc} + */ + public function getConfiguration() { + return $this->configuration; + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return [ + 'source_field' => '', + ]; + } + + /** + * {@inheritdoc} + */ + public function getMetadata(MediaInterface $media, $attribute_name) { + switch ($attribute_name) { + case 'default_name': + return 'media:' . $media->bundle() . ':' . $media->uuid(); + + case 'thumbnail_uri': + $default_thumbnail_filename = $this->pluginDefinition['default_thumbnail_filename']; + return $this->configFactory->get('media.settings')->get('icon_base_uri') . '/' . $default_thumbnail_filename; + } + + return NULL; + } + + /** + * {@inheritdoc} + */ + public function calculateDependencies() { + return []; + } + + /** + * Get the source field options for the media type form. + * + * This returns all fields related to media entities, filtered by the allowed + * field types in the media source annotation. + * + * @return string[] + * A list of source field options for the media type form. + */ + protected function getSourceFieldOptions() { + // 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(); + } + } + return $options; + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + $options = $this->getSourceFieldOptions(); + $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_type field will be automatically created on this type to store the essential information about the media item.', [ + '%field_type' => $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 (array_intersect_key($form_state->getValues(), $this->configuration) as $config_key => $config_value) { + $this->configuration[$config_key] = $config_value; + } + + // If no source field is explicitly set, create it now. + if (empty($this->configuration['source_field'])) { + $field_storage = $this->createSourceFieldStorage(); + $field_storage->save(); + $this->configuration['source_field'] = $field_storage->getName(); + } + } + + /** + * 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']), + 'locked' => TRUE, + ]); + } + + /** + * Returns the source field storage definition. + * + * @return \Drupal\Core\Field\FieldStorageDefinitionInterface|null + * The field storage definition or NULL if it doesn't exists. + */ + protected function getSourceFieldStorage() { + // Nothing to do if no source field is configured yet. + $field = $this->configuration['source_field']; + if ($field) { + // Even if we do know the name of the source field, there's no + // guarantee that it exists. + $fields = $this->entityFieldManager->getFieldStorageDefinitions('media'); + return isset($fields[$field]) ? $fields[$field] : NULL; + } + return NULL; + } + + /** + * {@inheritdoc} + */ + public function getSourceFieldDefinition(MediaTypeInterface $type) { + // Nothing to do if no source field is configured yet. + $field = $this->configuration['source_field']; + if ($field) { + // Even if we do know the name of the source field, there is no + // guarantee that it already exists. + $fields = $this->entityFieldManager->getFieldDefinitions('media', $type->id()); + return isset($fields[$field]) ? $fields[$field] : NULL; + } + return NULL; + } + + /** + * {@inheritdoc} + */ + public function createSourceField(MediaTypeInterface $type) { + $storage = $this->getSourceFieldStorage() ?: $this->createSourceFieldStorage(); + return $this->entityTypeManager + ->getStorage('field_config') + ->create([ + 'field_storage' => $storage, + '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() { + $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; + } + +} diff --git a/core/modules/media/src/MediaSourceEntityConstraintsInterface.php b/core/modules/media/src/MediaSourceEntityConstraintsInterface.php new file mode 100644 index 0000000000..29c6e92b37 --- /dev/null +++ b/core/modules/media/src/MediaSourceEntityConstraintsInterface.php @@ -0,0 +1,29 @@ +alterInfo('media_source_info'); + $this->setCacheBackend($cache_backend, 'media_source_plugins'); + } + +} diff --git a/core/modules/media/src/MediaTypeForm.php b/core/modules/media/src/MediaTypeForm.php new file mode 100644 index 0000000000..9ae84e9baa --- /dev/null +++ b/core/modules/media/src/MediaTypeForm.php @@ -0,0 +1,376 @@ +sourceManager = $source_manager; + $this->entityFieldManager = $entity_field_manager; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container) { + return new static( + $container->get('plugin.manager.media.source'), + $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('#source-dependent', $form['source_dependent'])); + return $response; + } + + /** + * {@inheritdoc} + */ + public function form(array $form, FormStateInterface $form_state) { + $form = parent::form($form, $form_state); + + // Source is not set when the entity is initially created. + /** @var \Drupal\media\MediaSourceInterface $source */ + $source = $this->entity->get('source') ? $this->entity->getSource() : 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->sourceManager->getDefinitions(); + $options = []; + foreach ($plugins as $plugin_id => $definition) { + $options[$plugin_id] = $definition['label']; + } + + $form['source_dependent'] = [ + '#type' => 'container', + '#attributes' => ['id' => 'source-dependent'], + ]; + + $form['source_dependent']['source'] = [ + '#type' => 'select', + '#title' => $this->t('Media source'), + '#default_value' => $source ? $source->getPluginId() : NULL, + '#options' => $options, + '#description' => $this->t('Media source that is responsible for additional logic related to this media type.'), + '#ajax' => ['callback' => '::ajaxHandlerData'], + '#required' => TRUE, + ]; + + if (!$source) { + $form['type']['#empty_option'] = $this->t('- Select media source -'); + } + + if ($source) { + // Media source plugin configuration. + $form['source_dependent']['source_configuration'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Media source configuration'), + '#tree' => TRUE, + ]; + + $form['source_dependent']['source_configuration'] = $source->buildConfigurationForm($form['source_dependent']['source_configuration'], $this->getSourceSubFormState($form, $form_state)); + } + + // Field mapping configuration. + $form['source_dependent']['field_map'] = [ + '#type' => 'fieldset', + '#title' => $this->t('Field mapping'), + '#tree' => TRUE, + 'description' => [ + '#markup' => '

' . $this->t('Media sources 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($source) || empty($source->getMetadataAttributes())) { + $form['source_dependent']['field_map']['#access'] = FALSE; + } + else { + $options = [MediaSourceInterface::METADATA_FIELD_EMPTY => $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 ($source->getMetadataAttributes() as $metadata_attribute_name => $metadata_attribute_label) { + $form['source_dependent']['field_map'][$metadata_attribute_name] = [ + '#type' => 'select', + '#title' => $metadata_attribute_label, + '#options' => $options, + '#default_value' => isset($field_map[$metadata_attribute_name]) ? $field_map[$metadata_attribute_name] : MediaSourceInterface::METADATA_FIELD_EMPTY, + ]; + } + } + + $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. When using remote media sources, the thumbnail generation could be a slow process. Using a queue allows for this process to be handled in the background.'); + + if ($this->moduleHandler->moduleExists('language')) { + $form['language'] = [ + '#type' => 'details', + '#title' => $this->t('Language settings'), + '#group' => 'additional_settings', + ]; + + $language_configuration = 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->thumbnailDownloadsAreQueued(), + ]; + // Prepare workflow options to be used for 'checkboxes' form element. + $keys = array_keys(array_filter($workflow_options)); + return array_combine($keys, $keys); + } + + /** + * Gets subform state for the media source configuration subform. + * + * @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 media source configuration form. + */ + protected function getSourceSubFormState(array $form, FormStateInterface $form_state) { + return SubformState::createForSubform($form['source_dependent']['source_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['source_dependent']['source_configuration']) { + // Let the selected plugin validate its settings. + $this->entity->getSource()->validateConfigurationForm($form['source_dependent']['source_configuration'], $this->getSourceSubFormState($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 != MediaSourceInterface::METADATA_FIELD_EMPTY; + } + )); + + parent::submitForm($form, $form_state); + + $this->entity->setQueueThumbnailDownloadsStatus((bool) $form_state->getValue(['options', 'queue_thumbnail_downloads'])) + ->setStatus((bool) $form_state->getValue(['options', 'status'])) + ->setNewRevision((bool) $form_state->getValue(['options', 'new_revision'])); + + if ($form['source_dependent']['source_configuration']) { + // Let the selected plugin save its settings. + $this->entity->getSource()->submitConfigurationForm($form['source_dependent']['source_configuration'], $this->getSourceSubFormState($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) { + $status = parent::save($form, $form_state); + /** @var \Drupal\media\MediaTypeInterface $media_type */ + $media_type = $this->entity; + + // If the media source is using a source field, ensure it's + // properly created. + $source = $media_type->getSource(); + $source_field = $source->getSourceFieldDefinition($media_type); + if (!$source_field) { + $source_field = $source->createSourceField($media_type); + /** @var \Drupal\field\FieldStorageConfigInterface $storage */ + $storage = $source_field->getFieldStorageDefinition(); + if ($storage->isNew() || !$storage->isLocked()) { + $storage->setLocked(TRUE)->save(); + } + $source_field->save(); + + // Add the new field to the default form and view displays for this + // media type. + $field_name = $source_field->getName(); + $field_type = $source_field->getType(); + + if ($source_field->isDisplayConfigurable('form')) { + // Use the default widget and settings. + $component = \Drupal::service('plugin.manager.field.widget') + ->prepareConfiguration($field_type, []); + + // @todo Replace entity_get_form_display() when #2367933 is done. + // https://www.drupal.org/node/2872159. + entity_get_form_display('media', $media_type->id(), 'default') + ->setComponent($field_name, $component) + ->save(); + } + if ($source_field->isDisplayConfigurable('view')) { + // Use the default formatter and settings. + $component = \Drupal::service('plugin.manager.field.formatter') + ->prepareConfiguration($field_type, []); + + // @todo Replace entity_get_display() when #2367933 is done. + // https://www.drupal.org/node/2872159. + entity_get_display('media', $media_type->id(), 'default') + ->setComponent($field_name, $component) + ->save(); + } + } + + $t_args = ['%name' => $media_type->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 media type %name.', $t_args); + } + + // Override the "status" base field default value, for this media type. + $fields = $this->entityFieldManager->getFieldDefinitions('media', $media_type->id()); + /** @var \Drupal\media\MediaInterface $media */ + $media = $this->entityTypeManager->getStorage('media')->create(['bundle' => $media_type->id()]); + $value = (bool) $form_state->getValue(['options', 'status']); + if ($media->status->value != $value) { + $fields['status']->getConfig($media_type->id())->setDefaultValue($value)->save(); + } + + $form_state->setRedirectUrl($media_type->toUrl('collection')); + } + +} diff --git a/core/modules/media/src/MediaTypeInterface.php b/core/modules/media/src/MediaTypeInterface.php new file mode 100644 index 0000000000..076ec00204 --- /dev/null +++ b/core/modules/media/src/MediaTypeInterface.php @@ -0,0 +1,97 @@ +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.', [ + ':url' => 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 0000000000..7a038cf16e --- /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 0000000000..56cf66c287 --- /dev/null +++ b/core/modules/media/src/Plugin/Action/PublishMedia.php @@ -0,0 +1,40 @@ +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 0000000000..b8ed0062dd --- /dev/null +++ b/core/modules/media/src/Plugin/Action/SaveMedia.php @@ -0,0 +1,40 @@ +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 0000000000..eb3b945ddb --- /dev/null +++ b/core/modules/media/src/Plugin/Action/UnpublishMedia.php @@ -0,0 +1,40 @@ +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 0000000000..5b3f8cc1dc --- /dev/null +++ b/core/modules/media/src/Plugin/Field/FieldFormatter/MediaThumbnailFormatter.php @@ -0,0 +1,201 @@ +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 item'), + ]; + // 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_items = $this->getEntitiesToView($items, $langcode); + + // Early opt-out if the field is empty. + if (empty($media_items)) { + return $elements; + } + + $image_style_setting = $this->getSetting('image_style'); + + /** @var \Drupal\media\MediaInterface[] $media_items */ + foreach ($media_items as $delta => $media) { + $elements[$delta] = [ + '#theme' => 'image_formatter', + '#item' => $media->get('thumbnail')->first(), + '#item_attributes' => [], + '#image_style' => $this->getSetting('image_style'), + '#url' => $this->getMediaThumbnailUrl($media, $items->getEntity()), + ]; + + // Add cacheability of each item in the field. + $this->renderer->addCacheableDependency($elements[$delta], $media); + } + + // Add cacheability of the image style setting. + if ($this->getSetting('image_link') && ($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 items. + return ($field_definition->getFieldStorageDefinition()->getSetting('target_type') == 'media'); + } + + /** + * Get the URL for the media thumbnail. + * + * @param \Drupal\media\MediaInterface $media + * The media item. + * @param \Drupal\Core\Entity\EntityInterface $entity + * The entity that the field belongs to. + * + * @return \Drupal\Core\Url|null + * The URL object for the media item or null if we don't want to add + * a link. + */ + protected function getMediaThumbnailUrl(MediaInterface $media, EntityInterface $entity) { + $url = NULL; + $image_link_setting = $this->getSetting('image_link'); + // Check if the formatter involves a link. + if ($image_link_setting == 'content') { + if (!$entity->isNew()) { + $url = $entity->toUrl(); + } + } + elseif ($image_link_setting === 'media') { + $url = $media->toUrl(); + } + return $url; + } + +} 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 0000000000..8ad8801978 --- /dev/null +++ b/core/modules/media/src/Plugin/QueueWorker/ThumbnailDownloader.php @@ -0,0 +1,68 @@ +entityTypeManager = $entity_type_manager; + } + + /** + * {@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') + ); + } + + /** + * {@inheritdoc} + */ + public function processItem($data) { + /** @var \Drupal\media\Entity\Media $media */ + if ($media = $this->entityTypeManager->getStorage('media')->load($data['id'])) { + $media->updateQueuedThumbnail(); + $media->save(); + } + } + +} 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 0000000000..eb457015dc --- /dev/null +++ b/core/modules/media/src/Plugin/views/wizard/Media.php @@ -0,0 +1,87 @@ + [ + 'value' => '1', + '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. + $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 0000000000..7abc41aad7 --- /dev/null +++ b/core/modules/media/src/Plugin/views/wizard/MediaRevision.php @@ -0,0 +1,99 @@ + [ + 'value' => '1', + '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']); + + // Add the changed field. + $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'] = ''; + + // Add the name field. + $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/templates/media.html.twig b/core/modules/media/templates/media.html.twig new file mode 100644 index 0000000000..762404e930 --- /dev/null +++ b/core/modules/media/templates/media.html.twig @@ -0,0 +1,48 @@ +{# +/** + * @file + * Default theme implementation to present a media item. + * + * Available variables: + * - media: The media item, with limited access to object properties and + * methods. Only method names starting with "get", "has", or "is" and + * a few common methods such as "id", "label", and "bundle" are available. + * For example: + * - entity.getEntityTypeId() will return the entity type ID. + * - entity.hasField('field_example') returns TRUE if the entity includes + * field_example. (This does not indicate the presence of a value in this + * field.) + * Calling other methods, such as entity.delete(), will result in + * an exception. + * See \Drupal\Core\Entity\EntityInterface for a full list of methods. + * - name: Name of the media item. + * - content: Media content. + * - title_prefix: Additional output populated by modules, intended to be + * displayed in front of the main title tag that appears in the template. + * - title_suffix: Additional output populated by modules, intended to be + * displayed after the main title tag that appears in the template. + * - view_mode: View mode; for example, "teaser" or "full". + * - attributes: HTML attributes for the containing element. + * - title_attributes: Same as attributes, except applied to the main title + * tag that appears in the template. + * + * @see template_preprocess_media() + * + * @ingroup themeable + */ +#} + + {# + In the 'full' view mode the entity label is assumed to be displayed as the + page title, so we do not display it here. + #} + {{ title_prefix }} + {% if label and view_mode != 'full' %} + + {{ label }} + + {% endif %} + {{ title_suffix }} + + {{ content }} + diff --git a/core/modules/media/tests/modules/media_test_source/config/schema/media_test_source.schema.yml b/core/modules/media/tests/modules/media_test_source/config/schema/media_test_source.schema.yml new file mode 100644 index 0000000000..089aeac35f --- /dev/null +++ b/core/modules/media/tests/modules/media_test_source/config/schema/media_test_source.schema.yml @@ -0,0 +1,15 @@ +media.source.test: + type: media.source.field_aware + label: 'Test media source configuration' + mapping: + test_config_value: + type: string + label: 'Test config value' + +media.source.test_translation: + type: media.source.test + label: 'Test media source with translations' + +media.source.test_constraints: + type: media.source.test + label: 'Test media source with constraints configuration' diff --git a/core/modules/media/tests/modules/media_test_source/media_test_source.info.yml b/core/modules/media/tests/modules/media_test_source/media_test_source.info.yml new file mode 100644 index 0000000000..c0a8736194 --- /dev/null +++ b/core/modules/media/tests/modules/media_test_source/media_test_source.info.yml @@ -0,0 +1,6 @@ +name: 'Test media source' +type: module +description: 'Provides test media source to test configuration forms.' +core: 8.x +package: Testing +version: VERSION diff --git a/core/modules/media/tests/modules/media_test_source/src/Plugin/Validation/Constraint/MediaTestConstraint.php b/core/modules/media/tests/modules/media_test_source/src/Plugin/Validation/Constraint/MediaTestConstraint.php new file mode 100644 index 0000000000..3d466b3e9e --- /dev/null +++ b/core/modules/media/tests/modules/media_test_source/src/Plugin/Validation/Constraint/MediaTestConstraint.php @@ -0,0 +1,25 @@ +label(); + } + elseif ($value instanceof FieldItemListInterface) { + $string_to_test = $value->value; + } + else { + return; + } + + if (strpos($string_to_test, 'love Drupal') === FALSE) { + $this->context->addViolation($constraint->message); + } + } + +} diff --git a/core/modules/media/tests/modules/media_test_source/src/Plugin/media/Source/Test.php b/core/modules/media/tests/modules/media_test_source/src/Plugin/media/Source/Test.php new file mode 100644 index 0000000000..b7baaa5557 --- /dev/null +++ b/core/modules/media/tests/modules/media_test_source/src/Plugin/media/Source/Test.php @@ -0,0 +1,88 @@ +get('media_source_test_attributes', [ + 'attribute_1' => ['label' => $this->t('Attribute 1'), 'value' => 'Value 1'], + 'attribute_2' => ['label' => $this->t('Attribute 2'), 'value' => 'Value 1'], + ]); + return array_map(function ($item) { + return $item['label']; + }, $attributes); + } + + /** + * {@inheritdoc} + */ + public function getMetadata(MediaInterface $media, $attribute_name) { + $attributes = \Drupal::state()->get('media_source_test_attributes', [ + 'attribute_1' => ['label' => $this->t('Attribute 1'), 'value' => 'Value 1'], + 'attribute_2' => ['label' => $this->t('Attribute 2'), 'value' => 'Value 1'], + ]); + + if (in_array($attribute_name, array_keys($attributes))) { + return $attributes[$attribute_name]['value']; + } + + return parent::getMetadata($media, $attribute_name); + } + + /** + * {@inheritdoc} + */ + public function getPluginDefinition() { + return NestedArray::mergeDeep( + parent::getPluginDefinition(), + \Drupal::state()->get('media_source_test_definition', []) + ); + } + + /** + * {@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' => $this->configuration['test_config_value'], + ]; + + return $form; + } + +} diff --git a/core/modules/media/tests/modules/media_test_source/src/Plugin/media/Source/TestTranslation.php b/core/modules/media/tests/modules/media_test_source/src/Plugin/media/Source/TestTranslation.php new file mode 100644 index 0000000000..947f06733b --- /dev/null +++ b/core/modules/media/tests/modules/media_test_source/src/Plugin/media/Source/TestTranslation.php @@ -0,0 +1,36 @@ +language()->getId() . '.png'; + } + + if ($attribute_name == 'test_thumbnail_alt') { + $langcode = $media->language()->getId(); + return $this->t('Test Thumbnail @language', ['@language' => $langcode], ['langcode' => $langcode]); + } + + return parent::getMetadata($media, $attribute_name); + } + +} diff --git a/core/modules/media/tests/modules/media_test_source/src/Plugin/media/Source/TestWithConstraints.php b/core/modules/media/tests/modules/media_test_source/src/Plugin/media/Source/TestWithConstraints.php new file mode 100644 index 0000000000..98039ca2d2 --- /dev/null +++ b/core/modules/media/tests/modules/media_test_source/src/Plugin/media/Source/TestWithConstraints.php @@ -0,0 +1,34 @@ +get('media_source_test_entity_constraints', []); + } + + /** + * {@inheritdoc} + */ + public function getSourceFieldConstraints() { + return \Drupal::state()->get('media_source_test_field_constraints', []); + } + +} 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 0000000000..96beb46ebf --- /dev/null +++ b/core/modules/media/tests/modules/media_test_type/config/install/media.type.test.yml @@ -0,0 +1,11 @@ +id: test +label: 'Test type' +description: 'Test type.' +source: test +source_configuration: + test_config_value: 'Kakec' +status: true +langcode: en +dependencies: { } +field_map: + metadata_attribute: 'field_attribute_config_test' 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 0000000000..7eec4e3530 --- /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 a media item.' +core: 8.x +package: Testing +version: VERSION +dependencies: + - media_test_source 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 0000000000..dd432bbfbd --- /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 0000000000..4962319b3b --- /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 0000000000..eae9bc489a --- /dev/null +++ b/core/modules/media/tests/src/Functional/MediaAccessTest.php @@ -0,0 +1,106 @@ +assertSession(); + + $media_type = $this->createMediaType(); + + // Create media. + $media = Media::create([ + 'bundle' => $media_type->id(), + 'name' => 'Unnamed', + ]); + $media->save(); + $user_media = Media::create([ + 'bundle' => $media_type->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()); + $this->assertCacheContext('user.permissions'); + $assert_session->statusCodeEquals(200); + $this->drupalGet('media/' . $user_media->id() . '/edit'); + $this->assertCacheContext('user.permissions'); + $assert_session->statusCodeEquals(200); + $this->drupalGet('media/' . $user_media->id() . '/delete'); + $this->assertCacheContext('user.permissions'); + $assert_session->statusCodeEquals(200); + + $this->drupalLogin($this->nonAdminUser); + /** @var \Drupal\user\RoleInterface $role */ + $role = Role::load(RoleInterface::AUTHENTICATED_ID); + + // Test 'view media' permission. + $this->drupalGet('media/' . $media->id()); + $this->assertCacheContext('user.permissions'); + $assert_session->statusCodeEquals(403); + $this->grantPermissions($role, ['view media']); + $this->drupalGet('media/' . $media->id()); + $this->assertCacheContext('user.permissions'); + $assert_session->statusCodeEquals(200); + + // Test 'create media' permission. + $this->drupalGet('media/add/' . $media_type->id()); + $this->assertCacheContext('user.permissions'); + $assert_session->statusCodeEquals(403); + $this->grantPermissions($role, ['create media']); + $this->drupalGet('media/add/' . $media_type->id()); + $this->assertCacheContext('user.permissions'); + $assert_session->statusCodeEquals(200); + + // Test 'update media' and 'delete media' permissions. + $this->drupalGet('media/' . $user_media->id() . '/edit'); + $this->assertCacheContext('user'); + $assert_session->statusCodeEquals(403); + $this->drupalGet('media/' . $user_media->id() . '/delete'); + $this->assertCacheContext('user'); + $assert_session->statusCodeEquals(403); + $this->grantPermissions($role, ['update media']); + $this->grantPermissions($role, ['delete media']); + $this->drupalGet('media/' . $user_media->id() . '/edit'); + $this->assertCacheContext('user'); + $assert_session->statusCodeEquals(200); + $this->drupalGet('media/' . $user_media->id() . '/delete'); + $this->assertCacheContext('user'); + $assert_session->statusCodeEquals(200); + + // Test 'update any media' and 'delete any media' permissions. + $this->drupalGet('media/' . $media->id() . '/edit'); + $this->assertCacheContext('user'); + $assert_session->statusCodeEquals(403); + $this->drupalGet('media/' . $media->id() . '/delete'); + $this->assertCacheContext('user'); + $assert_session->statusCodeEquals(403); + $this->grantPermissions($role, ['update any media']); + $this->grantPermissions($role, ['delete any media']); + $this->drupalGet('media/' . $media->id() . '/edit'); + $this->assertCacheContext('user.permissions'); + $assert_session->statusCodeEquals(200); + $this->drupalGet('media/' . $media->id() . '/delete'); + $this->assertCacheContext('user.permissions'); + $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 0000000000..cb9e340f99 --- /dev/null +++ b/core/modules/media/tests/src/Functional/MediaBulkFormTest.php @@ -0,0 +1,113 @@ +testMediaType = $this->createMediaType(); + + // Create some test media items. + $this->mediaItems = []; + for ($i = 1; $i <= 5; $i++) { + $media = Media::create([ + 'bundle' => $this->testMediaType->id(), + ]); + $media->save(); + $this->mediaItems[] = $media; + } + } + + /** + * Tests the media bulk form. + */ + public function testBulkForm() { + $session = $this->getSession(); + $page = $session->getPage(); + $assert_session = $this->assertSession(); + + // Check that all created items 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'); + $this->assertFalse($this->storage->loadUnchanged(1)->isPublished(), 'The unpublish action failed in some of the media items.'); + $this->assertFalse($this->storage->loadUnchanged(2)->isPublished(), 'The unpublish action failed in some of the media items.'); + $this->assertFalse($this->storage->loadUnchanged(3)->isPublished(), 'The unpublish action failed in some of the media items.'); + + // 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'); + $this->assertTrue($this->storage->loadUnchanged(1)->isPublished(), 'The publish action failed in some of the media items.'); + $this->assertTrue($this->storage->loadUnchanged(2)->isPublished(), 'The publish action failed in some of the media items.'); + + // 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 items.'); + $this->assertNull($this->storage->loadUnchanged(1), 'Could not delete some of the media items.'); + $this->assertNull($this->storage->loadUnchanged(2), 'Could not delete some of the media items.'); + } + +} diff --git a/core/modules/media/tests/src/Functional/MediaCacheTagsTest.php b/core/modules/media/tests/src/Functional/MediaCacheTagsTest.php new file mode 100644 index 0000000000..cdbfc292a1 --- /dev/null +++ b/core/modules/media/tests/src/Functional/MediaCacheTagsTest.php @@ -0,0 +1,77 @@ +grantPermission('view media'); + $user_role->save(); + } + + /** + * {@inheritdoc} + */ + protected function createEntity() { + // Create a media type. + $mediaType = $this->createMediaType(); + + // Create a media item. + $media = Media::create([ + 'bundle' => $mediaType->id(), + 'name' => 'Unnamed', + ]); + $media->save(); + + return $media; + } + + /** + * {@inheritdoc} + */ + protected function getAdditionalCacheContextsForEntity(EntityInterface $media) { + return ['timezone']; + } + + /** + * {@inheritdoc} + */ + protected function getAdditionalCacheTagsForEntity(EntityInterface $media) { + // Each media item must have an author and a thumbnail. + return [ + 'user:' . $media->getOwnerId(), + 'config:image.style.thumbnail', + 'file:' . $media->get('thumbnail')->entity->id(), + ]; + } + +} 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 0000000000..017e725e3b --- /dev/null +++ b/core/modules/media/tests/src/Functional/MediaFunctionalTestBase.php @@ -0,0 +1,29 @@ +randomMachineName()); + } + else { + $id = $values['bundle']; + } + $values += [ + 'id' => $id, + 'label' => $id, + 'source' => $source, + 'source_configuration' => [], + 'field_map' => [], + 'new_revision' => FALSE, + ]; + + $media_type = MediaType::create($values); + $status = $media_type->save(); + + // @todo Rename to assertSame() when #1945040 is done. + // @see https://www.drupal.org/node/1945040 + $this->assertIdentical(SAVED_NEW, $status, 'Media type was created successfully.'); + + // Ensure that the source field exists. + $source = $media_type->getSource(); + $source_field = $source->getSourceFieldDefinition($media_type); + if (!$source_field) { + $source_field = $source->createSourceField($media_type); + /** @var \Drupal\field\FieldStorageConfigInterface $storage */ + $storage = $source_field->getFieldStorageDefinition(); + $storage->setLocked(TRUE)->save(); + $source_field->save(); + + $media_type + ->set('source_configuration', [ + 'source_field' => $source_field->getName(), + ]) + ->save(); + } + + return $media_type; + } + +} 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 0000000000..75e0d06c16 --- /dev/null +++ b/core/modules/media/tests/src/Functional/MediaFunctionalTestTrait.php @@ -0,0 +1,74 @@ +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/MediaTemplateSuggestionsTest.php b/core/modules/media/tests/src/Functional/MediaTemplateSuggestionsTest.php new file mode 100644 index 0000000000..e0e5294122 --- /dev/null +++ b/core/modules/media/tests/src/Functional/MediaTemplateSuggestionsTest.php @@ -0,0 +1,46 @@ +createMediaType([ + 'new_revision' => FALSE, + 'queue_thumbnail_downloads' => FALSE, + ]); + + // Create media item to be rendered. + $media = Media::create([ + 'bundle' => $media_type->id(), + 'name' => 'Unnamed', + ]); + $media->save(); + $view_mode = 'full'; + + // Simulate theming of the media item. + $build = \Drupal::entityTypeManager()->getViewBuilder('media')->view($media, $view_mode); + + $variables['elements'] = $build; + $suggestions = \Drupal::moduleHandler()->invokeAll('theme_suggestions_media', [$variables]); + $this->assertEquals($suggestions, ['media__full', 'media__' . $media_type->id(), 'media__' . $media_type->id() . '__full'], 'Found expected media suggestions.'); + } + +} 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 0000000000..883f27ec10 --- /dev/null +++ b/core/modules/media/tests/src/Functional/MediaUiFunctionalTest.php @@ -0,0 +1,166 @@ +drupalPlaceBlock('local_actions_block'); + $this->drupalPlaceBlock('local_tasks_block'); + } + + /** + * Tests the media actions (add/edit/delete). + */ + public function testMediaWithOnlyOneMediaType() { + $session = $this->getSession(); + $page = $session->getPage(); + $assert_session = $this->assertSession(); + + $media_type = $this->createMediaType([ + 'new_revision' => FALSE, + 'queue_thumbnail_downloads' => FALSE, + ]); + + $this->drupalGet('media/add'); + $assert_session->statusCodeEquals(200); + $assert_session->addressEquals('media/add/' . $media_type->id()); + $assert_session->elementNotExists('css', '#edit-revision'); + + // Tests media add form. + $media_name = $this->randomMachineName(); + $page->fillField('name[0][value]', $media_name); + $revision_log_message = $this->randomString(); + $page->fillField('revision_log_message[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); + $this->assertEquals($media->label(), $media_name); + $assert_session->titleEquals($media_name . ' | Drupal'); + + // Tests media edit form. + $media_type->setNewRevision(FALSE); + $media_type->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'); + /** @var \Drupal\media\MediaInterface $media */ + $media = $this->container->get('entity_type.manager') + ->getStorage('media') + ->loadUnchanged($media_id); + $this->assertEquals($media->label(), $media_name2); + $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/' . $media_type->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'); + $assert_session->elementNotExists('css', 'input.vertical-tabs__active-tab'); + // Continue testing as admin. + $this->drupalLogin($this->adminUser); + + // Enable revisions by default. + $previous_revision_id = $media->getRevisionId(); + $media_type->setNewRevision(TRUE); + $media_type->save(); + $this->drupalGet('media/' . $media_id . '/edit'); + $assert_session->checkboxChecked('edit-revision'); + $page->fillField('name[0][value]', $media_name); + $page->fillField('revision_log_message[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); + $this->assertNotEquals($previous_revision_id, $media->getRevisionId()); + + // 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 types available. + */ + public function testMediaWithMultipleMediaTypes() { + $assert_session = $this->assertSession(); + + // Tests and creates the first media type. + $first_media_type = $this->createMediaType(['description' => $this->randomMachineName(32)]); + + // Test and create a second media type. + $second_media_type = $this->createMediaType(['description' => $this->randomMachineName(32)]); + + // Test if media/add displays two media type options. + $this->drupalGet('media/add'); + + // Checks for the first media type. + $assert_session->pageTextContains($first_media_type->label()); + $assert_session->pageTextContains($first_media_type->getDescription()); + // Checks for the second media type. + $assert_session->pageTextContains($second_media_type->label()); + $assert_session->pageTextContains($second_media_type->getDescription()); + + // Continue testing media type filter. + $first_media_item = Media::create(['bundle' => $first_media_type->id()]); + $first_media_item->save(); + $second_media_item = Media::create(['bundle' => $second_media_type->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/MediaJavascriptTestBase.php b/core/modules/media/tests/src/FunctionalJavascript/MediaJavascriptTestBase.php new file mode 100644 index 0000000000..7fc26ec96e --- /dev/null +++ b/core/modules/media/tests/src/FunctionalJavascript/MediaJavascriptTestBase.php @@ -0,0 +1,46 @@ + 0"; + $this->assertJsCondition($condition, $timeout, $message); + } + +} diff --git a/core/modules/media/tests/src/FunctionalJavascript/MediaTypeCreationTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaTypeCreationTest.php new file mode 100644 index 0000000000..bd263b5576 --- /dev/null +++ b/core/modules/media/tests/src/FunctionalJavascript/MediaTypeCreationTest.php @@ -0,0 +1,98 @@ +drupalGet('admin/structure/media/add'); + $page = $this->getSession()->getPage(); + + // Fill in a label to the media type. + $page->fillField('label', $label); + // Wait for machine name generation. Default: waitUntilVisible(), does not + // work properly. + $this->getSession() + ->wait(5000, "jQuery('.machine-name-value').text() === '{$mediaTypeMachineName}'"); + + // Select the media source used by our media type. + $this->assertSession()->fieldExists('Media source'); + $this->assertSession()->optionExists('Media source', 'test'); + $page->selectFieldOption('Media source', 'test'); + $this->assertSession()->assertWaitOnAjaxRequest(); + + $page->pressButton('Save'); + + // Check whether the source field was correctly created. + $this->drupalGet("admin/structure/media/manage/{$mediaTypeMachineName}/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 type. + $this->drupalGet("admin/structure/media/manage/{$mediaTypeMachineName}"); + + $this->assertSession()->pageTextContains('Test source field is used to store the essential information about the media item.'); + } + + /** + * Test creation of media type, reusing an existing source field. + */ + public function testMediaTypeCreationReuseSourceField() { + // Create a new media type, 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('Media source', 'test'); + $this->assertSession()->assertWaitOnAjaxRequest(); + $page->pressButton('Save'); + + $label = 'Type reusing Default Field'; + $mediaTypeMachineName = str_replace(' ', '_', strtolower($label)); + + $this->drupalGet('admin/structure/media/add'); + $page = $this->getSession()->getPage(); + + // Fill in a label to the media type. + $page->fillField('label', $label); + + // Wait for machine name generation. Default: waitUntilVisible(), does not + // work properly. + $this->getSession() + ->wait(5000, "jQuery('.machine-name-value').text() === '{$mediaTypeMachineName}'"); + + // Select the media source used by our media type. + $this->assertSession()->fieldExists('Media source'); + $this->assertSession()->optionExists('Media source', 'test'); + $page->selectFieldOption('Media source', 'test'); + $this->assertSession()->assertWaitOnAjaxRequest(); + // Select the existing field for re-use. + $page->selectFieldOption('source_configuration[source_field]', 'field_media_test'); + $page->pressButton('Save'); + + // Check that no new fields were created. + $this->drupalGet("admin/structure/media/manage/{$mediaTypeMachineName}/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/MediaUiJavascriptTest.php b/core/modules/media/tests/src/FunctionalJavascript/MediaUiJavascriptTest.php new file mode 100644 index 0000000000..fb98d90fb4 --- /dev/null +++ b/core/modules/media/tests/src/FunctionalJavascript/MediaUiJavascriptTest.php @@ -0,0 +1,199 @@ +drupalPlaceBlock('local_actions_block'); + $this->drupalPlaceBlock('local_tasks_block'); + } + + /** + * Tests a media type administration. + */ + public function testMediaTypes() { + $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 type 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('source', 'test'); + $this->assertJsCondition("jQuery('.form-item-source-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); + + // 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(); + // Assert that the field and field storage were created. + $media_type = MediaType::load($machine_name); + $source = $media_type->getSource(); + /** @var \Drupal\field\FieldConfigInterface $source_field */ + $source_field = $source->getSourceFieldDefinition($media_type); + $this->assertInstanceOf(FieldConfigInterface::class, $source_field, 'Source field exists.'); + $this->assertFalse($source_field->isNew(), 'Source field was saved.'); + /** @var \Drupal\field\FieldStorageConfigInterface $storage */ + $storage = $source_field->getFieldStorageDefinition(); + $this->assertFalse($storage->isNew(), 'Source field storage definition was saved.'); + $this->assertTrue($storage->isLocked(), 'Source field storage definition was locked.'); + + /** @var \Drupal\media\MediaTypeInterface $media_type_storage */ + $media_type_storage = $this->container->get('entity_type.manager')->getStorage('media_type'); + $this->testMediaType = $media_type_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->testMediaType->id()); + $assert_session->linkByHrefExists('admin/structure/media/manage/' . $this->testMediaType->id() . '/fields'); + $assert_session->linkByHrefExists('admin/structure/media/manage/' . $this->testMediaType->id() . '/form-display'); + $assert_session->linkByHrefExists('admin/structure/media/manage/' . $this->testMediaType->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('source', '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 sources 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('source', 'test'); + $assert_session->assertWaitOnAjaxRequest(); + $assert_session->fieldExists('Test config value'); + $assert_session->fieldValueEquals('Test config value', 'This is default value.'); + $assert_session->fieldExists('Attribute 1'); + $assert_session->fieldExists('Attribute 2'); + + // Test if the edit machine name is not editable. + $assert_session->fieldDisabled('Machine-readable name'); + + // Edit and save media type 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('source', 'test'); + $page->fillField('Test config value', 'This is new config value.'); + $page->selectFieldOption('field_map[attribute_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->testMediaType->id()); + $assert_session->fieldValueEquals('label', $new_name); + $assert_session->fieldValueEquals('description', $new_description); + $assert_session->fieldValueEquals('source', '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('Attribute 1', 'name'); + $assert_session->fieldValueEquals('Attribute 2', MediaSourceInterface::METADATA_FIELD_EMPTY); + + /** @var \Drupal\media\MediaTypeInterface $loaded_media_type */ + $loaded_media_type = $this->container->get('entity_type.manager') + ->getStorage('media_type') + ->load($this->testMediaType->id()); + $this->assertEquals($loaded_media_type->id(), $this->testMediaType->id()); + $this->assertEquals($loaded_media_type->label(), $new_name); + $this->assertEquals($loaded_media_type->getDescription(), $new_description); + $this->assertEquals($loaded_media_type->getSource()->getPluginId(), 'test'); + $this->assertEquals($loaded_media_type->getSource()->getConfiguration()['test_config_value'], 'This is new config value.'); + $this->assertTrue($loaded_media_type->shouldCreateNewRevision()); + $this->assertTrue($loaded_media_type->thumbnailDownloadsAreQueued()); + $this->assertFalse($loaded_media_type->getStatus()); + $this->assertEquals($loaded_media_type->getFieldMap(), ['attribute_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 item 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_media_type->id()]); + $this->assertFalse($unpublished_media->isPublished()); + $unpublished_media->delete(); + + // Tests media type delete form. + $page->clickLink('Delete'); + $assert_session->addressEquals('admin/structure/media/manage/' . $this->testMediaType->id() . '/delete'); + $page->pressButton('Delete'); + $assert_session->addressEquals('admin/structure/media'); + $assert_session->pageTextContains('The media type ' . $new_name . ' has been deleted.'); + + // Test that the system for preventing the deletion of media types works + // (they cannot be deleted if there is media content of that type/bundle). + $media_type2 = $this->createMediaType(); + $label2 = $media_type2->label(); + $media = Media::create(['name' => 'lorem ipsum', 'bundle' => $media_type2->id()]); + $media->save(); + $this->drupalGet('admin/structure/media/manage/' . $media_type2->id()); + $page->clickLink('Delete'); + $assert_session->addressEquals('admin/structure/media/manage/' . $media_type2->id() . '/delete'); + $assert_session->buttonNotExists('edit-submit'); + $assert_session->pageTextContains("$label2 is used by 1 media item on your site. You can not remove this media type until you have removed all of the $label2 media items."); + } + +} 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 0000000000..7b91c6ea3c --- /dev/null +++ b/core/modules/media/tests/src/FunctionalJavascript/MediaViewsWizardTest.php @@ -0,0 +1,86 @@ +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'); + $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'); + $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/MediaCreationTest.php b/core/modules/media/tests/src/Kernel/MediaCreationTest.php new file mode 100644 index 0000000000..123470341b --- /dev/null +++ b/core/modules/media/tests/src/Kernel/MediaCreationTest.php @@ -0,0 +1,58 @@ +container->get('entity_type.manager')->getStorage('media_type'); + + $this->assertInstanceOf(MediaTypeInterface::class, MediaType::load($this->testMediaType->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_media_type = $media_type_storage->load('test'); + $this->assertInstanceOf(MediaTypeInterface::class, $test_media_type, 'The media type from default configuration has not been created in the database.'); + $this->assertEquals('Test type', $test_media_type->get('label'), 'Could not assure the correct type name.'); + $this->assertEquals('Test type.', $test_media_type->get('description'), 'Could not assure the correct type description.'); + $this->assertEquals('test', $test_media_type->get('source'), 'Could not assure the correct media source.'); + // Source field is not set on the media source, but it should never + // be created automatically when a config is being imported. + $this->assertEquals(['source_field' => '', 'test_config_value' => 'Kakec'], $test_media_type->get('source_configuration'), 'Could not assure the correct media source configuration.'); + $this->assertEquals(['metadata_attribute' => 'field_attribute_config_test'], $test_media_type->get('field_map'), 'Could not assure the correct field map.'); + } + + /** + * Tests creating a media item programmatically. + */ + public function testMediaEntityCreation() { + $media = Media::create([ + 'bundle' => $this->testMediaType->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 item has not been created in the database.'); + $this->assertEquals($this->testMediaType->id(), $media->bundle(), 'The media item was not created with the correct type.'); + $this->assertEquals('Unnamed', $media->label(), 'The media item was not created with the correct name.'); + $source_field_name = $media->bundle->entity->getSource()->getSourceFieldDefinition($media->bundle->entity)->getName(); + $this->assertEquals('Nation of sheep, ruled by wolves, owned by pigs.', $media->get($source_field_name)->value, 'Source returns incorrect source field value.'); + } + +} diff --git a/core/modules/media/tests/src/Kernel/MediaKernelTestBase.php b/core/modules/media/tests/src/Kernel/MediaKernelTestBase.php new file mode 100644 index 0000000000..0daec876af --- /dev/null +++ b/core/modules/media/tests/src/Kernel/MediaKernelTestBase.php @@ -0,0 +1,91 @@ +installEntitySchema('user'); + $this->installEntitySchema('file'); + $this->installSchema('file', 'file_usage'); + $this->installEntitySchema('media'); + $this->installConfig(['field', 'system', 'image', 'file', 'media']); + + // Create a test media type. + $this->testMediaType = $this->createMediaType('test'); + // Create a test media type with constraints. + $this->testConstraintsMediaType = $this->createMediaType('test_constraints'); + } + + /** + * Create a media type for a source plugin. + * + * @param string $media_source_name + * The name of the media source. + * + * @return \Drupal\media\MediaTypeInterface + * A media type. + */ + protected function createMediaType($media_source_name) { + $id = strtolower($this->randomMachineName()); + $media_type = MediaType::create([ + 'id' => $id, + 'label' => $id, + 'source' => $media_source_name, + 'new_revision' => FALSE, + ]); + $media_type->save(); + $source_field = $media_type->getSource()->createSourceField($media_type); + // The media type form creates a source field if it does not exist yet. The + // same must be done in a kernel test, since it does not use that form. + // @see \Drupal\media\MediaTypeForm::save() + $source_field->getFieldStorageDefinition()->save(); + // The source field storage has been created, now the field can be saved. + $source_field->save(); + $media_type->set('source_configuration', [ + 'source_field' => $source_field->getName(), + ])->save(); + return $media_type; + } + +} diff --git a/core/modules/media/tests/src/Kernel/MediaSourceTest.php b/core/modules/media/tests/src/Kernel/MediaSourceTest.php new file mode 100644 index 0000000000..60065bf26a --- /dev/null +++ b/core/modules/media/tests/src/Kernel/MediaSourceTest.php @@ -0,0 +1,456 @@ + $this->testMediaType->id()]); + $media_source = $media->getSource(); + $this->assertEquals('default_name', $media_source->getPluginDefinition()['default_name_metadata_attribute'], 'Default metadata attribute is not used for the default name.'); + $this->assertEquals('media:' . $media->bundle() . ':' . $media->uuid(), $media_source->getMetadata($media, 'default_name'), 'Value of the default name metadata attribute does not look correct.'); + $media->save(); + $this->assertEquals('media:' . $media->bundle() . ':' . $media->uuid(), $media->label(), 'Default name was not set correctly.'); + + // Make sure that the user-supplied name is used. + /** @var \Drupal\media\MediaInterface $media */ + $name = 'User-supplied name'; + $media = Media::create([ + 'bundle' => $this->testMediaType->id(), + 'name' => $name, + ]); + $media_source = $media->getSource(); + $this->assertEquals('default_name', $media_source->getPluginDefinition()['default_name_metadata_attribute'], 'Default metadata attribute is not used for the default name.'); + $this->assertEquals('media:' . $media->bundle() . ':' . $media->uuid(), $media_source->getMetadata($media, 'default_name'), 'Value of the default name metadata attribute does not look correct.'); + $media->save(); + $this->assertEquals($name, $media->label(), 'User-supplied name was not set correctly.'); + + // Change the default name attribute and see if it is used to set the name. + $name = 'Old Major'; + \Drupal::state()->set('media_source_test_attributes', ['alternative_name' => ['title' => 'Alternative name', 'value' => $name]]); + \Drupal::state()->set('media_source_test_definition', ['default_name_metadata_attribute' => 'alternative_name']); + /** @var \Drupal\media\MediaInterface $media */ + $media = Media::create(['bundle' => $this->testMediaType->id()]); + $media_source = $media->getSource(); + $this->assertEquals('alternative_name', $media_source->getPluginDefinition()['default_name_metadata_attribute'], 'Correct metadata attribute is not used for the default name.'); + $this->assertEquals($name, $media_source->getMetadata($media, 'alternative_name'), 'Value of the default name metadata attribute does not look correct.'); + $media->save(); + $this->assertEquals($name, $media->label(), 'Default name was not set correctly.'); + } + + /** + * Tests metadata mapping functionality. + */ + public function testMetadataMapping() { + $field_name = 'field_to_map_to'; + $attribute_name = 'attribute_to_map'; + $storage = FieldStorageConfig::create([ + 'entity_type' => 'media', + 'field_name' => $field_name, + 'type' => 'string', + ]); + $storage->save(); + + FieldConfig::create([ + 'field_storage' => $storage, + 'bundle' => $this->testMediaType->id(), + 'label' => 'Field to map to', + ])->save(); + + // Save the entity without defining the metadata mapping and check that the + // field stays empty. + /** @var \Drupal\media\MediaInterface $media */ + $media = Media::create([ + 'bundle' => $this->testMediaType->id(), + 'field_media_test' => 'some_value', + ]); + $media->save(); + $this->assertEmpty($media->get($field_name)->value, 'Field stayed empty.'); + + // Make sure that source plugin returns NULL for non-existing fields. + $this->testMediaType->setFieldMap(['not_here_at_all' => $field_name])->save(); + $media = Media::create([ + 'bundle' => $this->testMediaType->id(), + 'field_media_test' => 'some_value', + ]); + $media_source = $media->getSource(); + $this->assertNull($media_source->getMetadata($media, 'not_here_at_all'), 'NULL is not returned if asking for a value of non-existing metadata.'); + $media->save(); + $this->assertTrue($media->get($field_name)->isEmpty(), 'Non-existing metadata attribute was wrongly mapped to the field.'); + + // Define mapping and make sure that the value was stored in the field. + \Drupal::state()->set('media_source_test_attributes', [ + $attribute_name => ['title' => 'Attribute to map', 'value' => 'Snowball'], + ]); + $this->testMediaType->setFieldMap([$attribute_name => $field_name])->save(); + $media = Media::create([ + 'bundle' => $this->testMediaType->id(), + 'field_media_test' => 'some_value', + ]); + $media_source = $media->getSource(); + $this->assertEquals('Snowball', $media_source->getMetadata($media, $attribute_name), 'Value of the metadata attribute is not correct.'); + $media->save(); + $this->assertEquals('Snowball', $media->get($field_name)->value, 'Metadata attribute was not mapped to the field.'); + + // Change the metadata attribute value and re-save the entity. Field value + // should stay the same. + \Drupal::state()->set('media_source_test_attributes', [ + $attribute_name => ['title' => 'Attribute to map', 'value' => 'Pinkeye'], + ]); + $this->assertEquals('Pinkeye', $media_source->getMetadata($media, $attribute_name), 'Value of the metadata attribute is not correct.'); + $media->save(); + $this->assertEquals('Snowball', $media->get($field_name)->value, 'Metadata attribute was not mapped to the field.'); + + // Now change the value of the source field and make sure that the mapped + // values update too. + $this->assertEquals('Pinkeye', $media_source->getMetadata($media, $attribute_name), 'Value of the metadata attribute is not correct.'); + $media->set('field_media_test', 'some_new_value'); + $media->save(); + $this->assertEquals('Pinkeye', $media->get($field_name)->value, 'Metadata attribute was not mapped to the field.'); + + // Remove the value of the mapped field and make sure that it is re-mapped + // on save. + \Drupal::state()->set('media_source_test_attributes', [ + $attribute_name => ['title' => 'Attribute to map', 'value' => 'Snowball'], + ]); + $media->{$field_name}->value = NULL; + $this->assertEquals('Snowball', $media_source->getMetadata($media, $attribute_name), 'Value of the metadata attribute is not correct.'); + $media->save(); + $this->assertEquals('Snowball', $media->get($field_name)->value, 'Metadata attribute was not mapped to the field.'); + } + + /** + * Tests the thumbnail functionality. + */ + public function testThumbnail() { + file_put_contents('public://thumbnail1.jpg', ''); + file_put_contents('public://thumbnail2.jpg', ''); + + // Save a media entity and make sure thumbnail was added. + \Drupal::state()->set('media_source_test_attributes', [ + 'thumbnail_uri' => ['title' => 'Thumbnail', 'value' => 'public://thumbnail1.jpg'], + ]); + /** @var \Drupal\media\MediaInterface $media */ + $media = Media::create([ + 'bundle' => $this->testMediaType->id(), + 'name' => 'Mr. Jones', + 'field_media_test' => 'some_value', + ]); + $media_source = $media->getSource(); + $this->assertEquals('public://thumbnail1.jpg', $media_source->getMetadata($media, 'thumbnail_uri'), 'Value of the thumbnail metadata attribute is not correct.'); + $media->save(); + $this->assertEquals('public://thumbnail1.jpg', $media->thumbnail->entity->getFileUri(), 'Thumbnail was not added to the media entity.'); + $this->assertEquals('Mr. Jones', $media->thumbnail->title, 'Title text was not set on the thumbnail.'); + $this->assertEquals('Thumbnail', $media->thumbnail->alt, 'Alt text was not set on the thumbnail.'); + + // Now change the metadata attribute and make sure that the thumbnail stays + // the same. + \Drupal::state()->set('media_source_test_attributes', [ + 'thumbnail_uri' => ['title' => 'Thumbnail', 'value' => 'public://thumbnail2.jpg'], + ]); + $this->assertEquals('public://thumbnail2.jpg', $media_source->getMetadata($media, 'thumbnail_uri'), 'Value of the thumbnail metadata attribute is not correct.'); + $media->save(); + $this->assertEquals('public://thumbnail1.jpg', $media->thumbnail->entity->getFileUri(), 'Thumbnail was not preserved.'); + $this->assertEquals('Mr. Jones', $media->thumbnail->title, 'Title text was not set on the thumbnail.'); + $this->assertEquals('Thumbnail', $media->thumbnail->alt, 'Alt text was not set on the thumbnail.'); + + // Remove the thumbnail and make sure that it is auto-updated on save. + $media->thumbnail->target_id = NULL; + $this->assertEquals('public://thumbnail2.jpg', $media_source->getMetadata($media, 'thumbnail_uri'), 'Value of the thumbnail metadata attribute is not correct.'); + $media->save(); + $this->assertEquals('public://thumbnail2.jpg', $media->thumbnail->entity->getFileUri(), 'New thumbnail was not added to the media entity.'); + $this->assertEquals('Mr. Jones', $media->thumbnail->title, 'Title text was not set on the thumbnail.'); + $this->assertEquals('Thumbnail', $media->thumbnail->alt, 'Alt text was not set on the thumbnail.'); + + // Change the metadata attribute again, change the source field value too + // and make sure that the thumbnail updates. + \Drupal::state()->set('media_source_test_attributes', [ + 'thumbnail_uri' => ['title' => 'Thumbnail', 'value' => 'public://thumbnail1.jpg'], + ]); + $media->field_media_test->value = 'some_new_value'; + $this->assertEquals('public://thumbnail1.jpg', $media_source->getMetadata($media, 'thumbnail_uri'), 'Value of the thumbnail metadata attribute is not correct.'); + $media->save(); + $this->assertEquals('public://thumbnail1.jpg', $media->thumbnail->entity->getFileUri(), 'New thumbnail was not added to the media entity.'); + $this->assertEquals('Mr. Jones', $media->thumbnail->title, 'Title text was not set on the thumbnail.'); + $this->assertEquals('Thumbnail', $media->thumbnail->alt, 'Alt text was not set on the thumbnail.'); + + // Change the thumbnail metadata attribute and make sure that the thumbnail + // is set correctly. + \Drupal::state()->set('media_source_test_attributes', [ + 'thumbnail_uri' => ['title' => 'Should not be used', 'value' => 'public://thumbnail1.jpg'], + 'alternative_thumbnail_uri' => ['title' => 'Should be used', 'value' => 'public://thumbnail2.jpg'], + ]); + \Drupal::state()->set('media_source_test_definition', ['thumbnail_uri_metadata_attribute' => 'alternative_thumbnail_uri']); + $media = Media::create([ + 'bundle' => $this->testMediaType->id(), + 'name' => 'Mr. Jones', + 'field_media_test' => 'some_value', + ]); + $media_source = $media->getSource(); + $this->assertEquals('public://thumbnail1.jpg', $media_source->getMetadata($media, 'thumbnail_uri'), 'Value of the metadata attribute is not correct.'); + $this->assertEquals('public://thumbnail2.jpg', $media_source->getMetadata($media, 'alternative_thumbnail_uri'), 'Value of the thumbnail metadata attribute is not correct.'); + $media->save(); + $this->assertEquals('public://thumbnail2.jpg', $media->thumbnail->entity->getFileUri(), 'Correct metadata attribute was not used for the thumbnail.'); + $this->assertEquals('Mr. Jones', $media->thumbnail->title, 'Title text was not set on the thumbnail.'); + $this->assertEquals('Thumbnail', $media->thumbnail->alt, 'Alt text was not set on the thumbnail.'); + + // Enable queued thumbnails and make sure that the entity gets the default + // thumbnail initially. + \Drupal::state()->set('media_source_test_definition', []); + \Drupal::state()->set('media_source_test_attributes', [ + 'thumbnail_uri' => ['title' => 'Should not be used', 'value' => 'public://thumbnail1.jpg'], + ]); + $this->testMediaType->setQueueThumbnailDownloadsStatus(TRUE)->save(); + $media = Media::create([ + 'bundle' => $this->testMediaType->id(), + 'name' => 'Mr. Jones', + 'field_media_test' => 'some_value', + ]); + $this->assertEquals('public://thumbnail1.jpg', $media->getSource()->getMetadata($media, 'thumbnail_uri'), 'Value of the metadata attribute is not correct.'); + $media->save(); + $this->assertEquals('public://media-icons/generic/generic.png', $media->thumbnail->entity->getFileUri(), 'Default thumbnail was not set initially.'); + $this->assertEquals('Mr. Jones', $media->thumbnail->title, 'Title text was not set on the thumbnail.'); + $this->assertEquals('Thumbnail', $media->thumbnail->alt, 'Alt text was not set on the thumbnail.'); + + // Process the queue item and make sure that the thumbnail was updated too. + $queue_name = 'media_entity_thumbnail'; + /** @var \Drupal\Core\Queue\QueueWorkerInterface $queue_worker */ + $queue_worker = \Drupal::service('plugin.manager.queue_worker')->createInstance($queue_name); + $queue = \Drupal::queue($queue_name); + $this->assertEquals(1, $queue->numberOfItems(), 'Item was not added to the queue.'); + + $item = $queue->claimItem(); + $this->assertEquals($media->id(), $item->data['id'], 'Queue item that was created does not belong to the correct entity.'); + + $queue_worker->processItem($item->data); + $queue->deleteItem($item); + $this->assertEquals(0, $queue->numberOfItems(), 'Item was not removed from the queue.'); + + $media = Media::load($media->id()); + $this->assertEquals('public://thumbnail1.jpg', $media->thumbnail->entity->getFileUri(), 'Thumbnail was not updated by the queue.'); + $this->assertEquals('Mr. Jones', $media->thumbnail->title, 'Title text was not set on the thumbnail.'); + $this->assertEquals('Thumbnail', $media->thumbnail->alt, 'Alt text was not set on the thumbnail.'); + + // Set alt and title metadata attributes and make sure they are used for the + // thumbnail. + \Drupal::state()->set('media_source_test_definition', [ + 'thumbnail_alt_metadata_attribute' => 'alt', + 'thumbnail_title_metadata_attribute' => 'title', + ]); + \Drupal::state()->set('media_source_test_attributes', [ + 'alt' => ['title' => 'Alt text', 'value' => 'This will be alt.'], + 'title' => ['title' => 'Title text', 'value' => 'This will be title.'], + ]); + $media = Media::create([ + 'bundle' => $this->testMediaType->id(), + 'name' => 'Boxer', + 'field_media_test' => 'some_value', + ]); + $media->save(); + $this->assertEquals('Boxer', $media->label(), 'Correct name was not set on the media entity.'); + $this->assertEquals('This will be title.', $media->thumbnail->title, 'Title text was not set on the thumbnail.'); + $this->assertEquals('This will be alt.', $media->thumbnail->alt, 'Alt text was not set on the thumbnail.'); + } + + /** + * Tests the media entity constraints functionality. + */ + public function testConstraints() { + // Test entity constraints. + \Drupal::state()->set('media_source_test_entity_constraints', [ + 'MediaTestConstraint' => [], + ]); + + // Create media that uses source plugin with constraints and make sure it + // can't be saved without validating them. + /** @var \Drupal\media\MediaInterface $media */ + $media = Media::create([ + 'bundle' => $this->testConstraintsMediaType->id(), + 'name' => 'I do not like Drupal', + 'field_media_test_constraints' => 'Not checked', + ]); + try { + $media->save(); + $this->fail('Save was allowed without validation.'); + } + catch (EntityStorageException $exception) { + $this->assertEquals('Entity validation was skipped.', $exception->getMessage(), 'Incorrect validation message.'); + $this->assertTrue(TRUE, 'Validation was enforced before save.'); + } + + // Validate the entity and make sure violation is reported. + /** @var \Drupal\Core\Entity\EntityConstraintViolationListInterface $violations */ + $violations = $media->validate(); + $this->assertCount(1, $violations, 'Expected number of validations not found.'); + $this->assertEquals('Inappropriate text.', $violations->get(0)->getMessage(), 'Incorrect constraint validation message found.'); + + // Fix the violation and make sure it is not reported anymore. + $media->set('name', 'I love Drupal!'); + $violations = $media->validate(); + $this->assertCount(0, $violations, 'Expected number of validations not found.'); + + // Save and make sure it succeeded. + $this->assertEmpty($media->id(), 'Entity ID was found.'); + $media->save(); + $this->assertNotEmpty($media->id(), 'Entity ID was not found.'); + + // Test source field constraints. + \Drupal::state()->set('media_source_test_field_constraints', [ + 'MediaTestConstraint' => [], + ]); + \Drupal::state()->set('media_source_test_entity_constraints', []); + + // Create media that uses source with constraints and make sure it can't + // be saved without validating them. + /** @var \Drupal\media\MediaInterface $media */ + $media = Media::create([ + 'bundle' => $this->testConstraintsMediaType->id(), + 'name' => 'Not checked', + 'field_media_test_constraints' => 'I do not like Drupal', + ]); + try { + $media->save(); + $this->fail('Save was allowed without validation.'); + } + catch (EntityStorageException $exception) { + $this->assertEquals('Entity validation was skipped.', $exception->getMessage(), 'Incorrect validation message.'); + $this->assertTrue(TRUE, 'Validation was enforced before save.'); + } + + // Validate the entity and make sure violation is reported. + /** @var \Drupal\Core\Entity\EntityConstraintViolationListInterface $violations */ + $violations = $media->validate(); + $this->assertCount(1, $violations, 'Expected number of validations not found.'); + $this->assertEquals('Inappropriate text.', $violations->get(0)->getMessage(), 'Incorrect constraint validation message found.'); + + // Fix the violation and make sure it is not reported anymore. + $media->set('field_media_test_constraints', 'I love Drupal!'); + $violations = $media->validate(); + $this->assertCount(0, $violations, 'Expected number of validations not found.'); + + // Save and make sure it succeeded. + $this->assertEmpty($media->id(), 'Entity ID was found.'); + $media->save(); + $this->assertNotEmpty($media->id(), 'Entity ID was not found.'); + } + + /** + * Tests logic related to the automated source field creation. + */ + public function testSourceFieldCreation() { + /** @var \Drupal\media\MediaTypeInterface $type */ + $type = MediaType::create([ + 'id' => 'test_type', + 'label' => 'Test type', + 'source' => 'test', + ]); + + /** @var \Drupal\field\Entity\FieldConfig $field */ + $field = $type->getSource()->createSourceField($type); + /** @var \Drupal\field\Entity\FieldStorageConfig $field_storage */ + $field_storage = $field->getFieldStorageDefinition(); + + // Test field storage. + $this->assertTrue($field_storage->isNew(), 'Field storage is saved automatically.'); + $this->assertTrue($field_storage->isLocked(), 'Field storage is not locked.'); + $this->assertEquals('string', $field_storage->getType(), 'Field is not of correct type.'); + $this->assertEquals('field_media_test_1', $field_storage->getName(), 'Incorrect field name is used.'); + $this->assertEquals('media', $field_storage->getTargetEntityTypeId(), 'Field is not targeting media entities.'); + + // Test field. + $this->assertTrue($field->isNew(), 'Field is saved automatically.'); + $this->assertEquals('field_media_test_1', $field->getName(), 'Incorrect field name is used.'); + $this->assertEquals('string', $field->getType(), 'Field is of incorrect type.'); + $this->assertTrue($field->isRequired(), 'Field is not required.'); + $this->assertEquals('Test source', $field->label(), 'Incorrect label is used.'); + $this->assertEquals('test_type', $field->getTargetBundle(), 'Field is not targeting correct bundle.'); + + // Fields should be automatically saved only when creating the media type + // using the media type creation form. Make sure that they are not saved + // when creating a media type programmatically. + // Drupal\Tests\media\FunctionalJavascript\MediaTypeCreationTest is testing + // form part of the functionality. + $type->save(); + $storage = FieldStorageConfig::load('media.field_media_test_1'); + $this->assertNull($storage, 'Field storage was not saved.'); + $field = FieldConfig::load('media.test_type.field_media_test_1'); + $this->assertNull($field, 'Field storage was not saved.'); + + // Test the plugin with a different default source field type. + $type = MediaType::create([ + 'id' => 'test_constraints_type', + 'label' => 'Test type with constraints', + 'source' => 'test_constraints', + ]); + $field = $type->getSource()->createSourceField($type); + $field_storage = $field->getFieldStorageDefinition(); + + // Test field storage. + $this->assertTrue($field_storage->isNew(), 'Field storage is saved automatically.'); + $this->assertTrue($field_storage->isLocked(), 'Field storage is not locked.'); + $this->assertEquals('string_long', $field_storage->getType(), 'Field is of incorrect type.'); + $this->assertEquals('field_media_test_constraints_1', $field_storage->getName(), 'Incorrect field name is used.'); + $this->assertEquals('media', $field_storage->getTargetEntityTypeId(), 'Field is not targeting media entities.'); + + // Test field. + $this->assertTrue($field->isNew(), 'Field is saved automatically.'); + $this->assertEquals('field_media_test_constraints_1', $field->getName(), 'Incorrect field name is used.'); + $this->assertEquals('string_long', $field->getType(), 'Field is of incorrect type.'); + $this->assertTrue($field->isRequired(), 'Field is not required.'); + $this->assertEquals('Test source with constraints', $field->label(), 'Incorrect label is used.'); + $this->assertEquals('test_constraints_type', $field->getTargetBundle(), 'Field is not targeting correct bundle.'); + } + + /** + * Tests configuration form submit handler on the base media source plugin. + */ + public function testSourceConfigurationSubmit() { + /** @var \Drupal\media\MediaSourceManager $manager */ + $manager = $this->container->get('plugin.manager.media.source'); + $form = []; + $form_state = new FormState(); + $form_state->setValues(['test_config_value' => 'Somewhere over the rainbow.']); + + /** @var \Drupal\media\MediaSourceInterface $source */ + $source = $manager->createInstance('test', []); + $source->submitConfigurationForm($form, $form_state); + $expected = ['test_config_value' => 'Somewhere over the rainbow.', 'source_field' => 'field_media_test_1']; + $this->assertEquals($expected, $source->getConfiguration(), 'Submitted values were saved correctly.'); + + // Try to save a NULL value. + $form_state->setValue('test_config_value', NULL); + $source->submitConfigurationForm($form, $form_state); + $expected['test_config_value'] = NULL; + $this->assertEquals($expected, $source->getConfiguration(), 'Submitted values were saved correctly.'); + + // Make sure that the config keys are determined correctly even if the + // existing value is NULL. + $form_state->setValue('test_config_value', 'Somewhere over the rainbow.'); + $source->submitConfigurationForm($form, $form_state); + $expected['test_config_value'] = 'Somewhere over the rainbow.'; + $this->assertEquals($expected, $source->getConfiguration(), 'Submitted values were saved correctly.'); + + // Make sure that a non-relevant value will be skipped. + $form_state->setValue('not_relevant', 'Should not be saved in the plugin.'); + $source->submitConfigurationForm($form, $form_state); + $this->assertEquals($expected, $source->getConfiguration(), 'Submitted values were saved correctly.'); + } + +} diff --git a/core/modules/media/tests/src/Kernel/MediaTokensTest.php b/core/modules/media/tests/src/Kernel/MediaTokensTest.php new file mode 100644 index 0000000000..a7222c0074 --- /dev/null +++ b/core/modules/media/tests/src/Kernel/MediaTokensTest.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 type. + $media_type_name = $this->randomMachineName(); + + MediaType::create([ + 'id' => $media_type_name, + 'label' => $media_type_name, + 'source' => 'test', + 'source_configuration' => ['test_config_value' => 'Kakec'], + 'field_map' => [], + 'status' => 1, + 'new_revision' => FALSE, + ])->save(); + + // Create a media item. + $media = Media::create([ + 'name' => $this->randomMachineName(), + 'bundle' => $media_type_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/modules/media/tests/src/Kernel/MediaTranslationTest.php b/core/modules/media/tests/src/Kernel/MediaTranslationTest.php new file mode 100644 index 0000000000..009e7c4d6b --- /dev/null +++ b/core/modules/media/tests/src/Kernel/MediaTranslationTest.php @@ -0,0 +1,103 @@ +installConfig(['language']); + + // Create a test media type for translations. + $this->testTranslationMediaType = $this->createMediaType('test_translation'); + + for ($i = 0; $i < 3; ++$i) { + $language_id = 'l' . $i; + ConfigurableLanguage::create([ + 'id' => $language_id, + 'label' => $this->randomString(), + ])->save(); + file_put_contents('public://' . $language_id . '.png', ''); + } + } + + /** + * Test translatable fields storage/retrieval. + */ + public function testTranslatableFieldSaveLoad() { + /** @var \Drupal\Core\Entity\EntityTypeInterface $entity_type */ + $entity_type = $this->container->get('entity_type.manager')->getDefinition('media'); + $this->assertTrue($entity_type->isTranslatable(), 'Media is translatable.'); + + // Prepare the field translations. + $source_field_definition = $this->testTranslationMediaType->getSource()->getSourceFieldDefinition($this->testTranslationMediaType); + $source_field_storage = $source_field_definition->getFieldStorageDefinition(); + /** @var \Drupal\Core\Entity\Sql\SqlContentEntityStorage $media_storage */ + $media_storage = $this->container->get('entity_type.manager')->getStorage('media'); + /** @var \Drupal\media\Entity\Media $media */ + $media = $media_storage->create([ + 'bundle' => $this->testTranslationMediaType->id(), + 'name' => 'Unnamed', + ]); + + $field_translations = []; + $available_langcodes = array_keys($this->container->get('language_manager')->getLanguages()); + $media->set('langcode', reset($available_langcodes)); + foreach ($available_langcodes as $langcode) { + $values = []; + for ($i = 0; $i < $source_field_storage->getCardinality(); $i++) { + $values[$i]['value'] = $this->randomString(); + } + $field_translations[$langcode] = $values; + $translation = $media->hasTranslation($langcode) ? $media->getTranslation($langcode) : $media->addTranslation($langcode); + $translation->{$source_field_definition->getName()}->setValue($field_translations[$langcode]); + } + + // Save and reload the field translations. + $media->save(); + $media_storage->resetCache(); + $media = $media_storage->load($media->id()); + + // Check if the correct source field values were saved/loaded. + foreach ($field_translations as $langcode => $items) { + /** @var \Drupal\media\MediaInterface $media_translation */ + $media_translation = $media->getTranslation($langcode); + $result = TRUE; + foreach ($items as $delta => $item) { + $result = $result && $item['value'] == $media_translation->{$source_field_definition->getName()}[$delta]->value; + } + $this->assertTrue($result, new FormattableMarkup('%language translation field value not correct.', ['%language' => $langcode])); + $this->assertEquals('public://' . $langcode . '.png', $media_translation->getSource()->getMetadata($media_translation, 'thumbnail_uri'), new FormattableMarkup('%language translation thumbnail metadata attribute is not correct.', ['%language' => $langcode])); + $this->assertEquals('public://' . $langcode . '.png', $media_translation->get('thumbnail')->entity->getFileUri(), new FormattableMarkup('%language translation thumbnail value is not correct.', ['%language' => $langcode])); + $this->assertEquals('Test Thumbnail ' . $langcode, $media_translation->getSource()->getMetadata($media_translation, 'test_thumbnail_alt'), new FormattableMarkup('%language translation thumbnail alt metadata attribute is not correct.', ['%language' => $langcode])); + $this->assertEquals('Test Thumbnail ' . $langcode, $media_translation->get('thumbnail')->alt, new FormattableMarkup('%language translation thumbnail alt value is not correct.', ['%language' => $langcode])); + } + } + +} diff --git a/core/modules/path/path.module b/core/modules/path/path.module index a724b773d6..8720a89319 100644 --- a/core/modules/path/path.module +++ b/core/modules/path/path.module @@ -62,7 +62,7 @@ function path_form_node_form_alter(&$form, FormStateInterface $form_state) { * Implements hook_entity_base_field_info(). */ function path_entity_base_field_info(EntityTypeInterface $entity_type) { - if ($entity_type->id() === 'taxonomy_term' || $entity_type->id() === 'node') { + if (in_array($entity_type->id(), ['taxonomy_term', 'node', 'media'])) { $fields['path'] = BaseFieldDefinition::create('path') ->setLabel(t('URL alias')) ->setTranslatable(TRUE) 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 0000000000..f36975cb9a --- /dev/null +++ b/core/themes/classy/templates/content/media.html.twig @@ -0,0 +1,27 @@ +{# +/** + * @file + * Theme override to display a media item. + * + * 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 0000000000..769b7be751 --- /dev/null +++ b/core/themes/stable/templates/content/media.html.twig @@ -0,0 +1,19 @@ +{# +/** + * @file + * Theme override to display a media item. + * + * Available variables: + * - name: Name of the media. + * - content: Media content. + * + * @see template_preprocess_media() + * + * @ingroup themeable + */ +#} + + {% if content %} + {{ content }} + {% endif %} +