diff --git a/core/composer.json b/core/composer.json index 4b5de2e..de58d07 100644 --- a/core/composer.json +++ b/core/composer.json @@ -110,6 +110,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 0000000..dfdbb3a --- /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 0000000..853e575 --- /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 0000000..62af35e --- /dev/null +++ b/core/modules/media/config/optional/system.action.media_delete_action.yml @@ -0,0 +1,10 @@ +langcode: en +status: true +dependencies: + module: + - media +id: media_delete_action +label: 'Delete media' +type: media +plugin: media_delete_action +configuration: { } diff --git a/core/modules/media/config/optional/system.action.media_publish_action.yml b/core/modules/media/config/optional/system.action.media_publish_action.yml new file mode 100644 index 0000000..95e173d --- /dev/null +++ b/core/modules/media/config/optional/system.action.media_publish_action.yml @@ -0,0 +1,10 @@ +langcode: en +status: true +dependencies: + module: + - media +id: media_publish_action +label: 'Publish media' +type: media +plugin: media_publish_action +configuration: { } diff --git a/core/modules/media/config/optional/system.action.media_save_action.yml b/core/modules/media/config/optional/system.action.media_save_action.yml new file mode 100644 index 0000000..c4d098f --- /dev/null +++ b/core/modules/media/config/optional/system.action.media_save_action.yml @@ -0,0 +1,10 @@ +langcode: en +status: true +dependencies: + module: + - media +id: media_save_action +label: 'Save media' +type: media +plugin: media_save_action +configuration: { } diff --git a/core/modules/media/config/optional/system.action.media_unpublish_action.yml b/core/modules/media/config/optional/system.action.media_unpublish_action.yml new file mode 100644 index 0000000..4189e4e --- /dev/null +++ b/core/modules/media/config/optional/system.action.media_unpublish_action.yml @@ -0,0 +1,10 @@ +langcode: en +status: true +dependencies: + module: + - media +id: media_unpublish_action +label: 'Unpublish media' +type: media +plugin: media_unpublish_action +configuration: { } diff --git a/core/modules/media/config/optional/views.view.media.yml b/core/modules/media/config/optional/views.view.media.yml new file mode 100644 index 0000000..745420c --- /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 0000000..4783f6d --- /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 0000000..0c7371d --- /dev/null +++ b/core/modules/media/config/schema/media.views.schema.yml @@ -0,0 +1,5 @@ +# Schema for the views plugins of the Media module. + +views.field.media_bulk_form: + type: views_field_bulk_form + label: 'Media bulk form' diff --git a/core/modules/media/images/icons/generic.png b/core/modules/media/images/icons/generic.png new file mode 100644 index 0000000..062b154 --- /dev/null +++ b/core/modules/media/images/icons/generic.png @@ -0,0 +1,4 @@ +PNG + + IHDR Y dIDATx0@ޠ(333333,[D߉y$hР 4hI4hРA 4hРA 4hРAkۀ}6ņ訆Dllр6\C!{ޒvHmG5AQ Q_.4" +3Y320zBfi"3s]Γ@*⋹vi"h>N-z\[%eY*hi=7bM`7[\GD s}v۷ou=%RAђ{'|ˣD{*44hРAVoّ#]к<-kAjMuˣX./~4hйFVg*W+VͷV}}ۧ+2Ft*B}8Ճ[7v\gOL9I|hQo# ZjWE+ٻUzy:#/̤#DРANI4Et4hOǀˀ- }keks!#Ѡ7ȵǎ^n#;xk#8quW7#պ˓@` 4hРA 4hРA 4hРA 4hР7 +F* IENDB` \ No newline at end of file diff --git a/core/modules/media/js/media_form.js b/core/modules/media/js/media_form.js new file mode 100644 index 0000000..c93eac6 --- /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 0000000..a6d9051 --- /dev/null +++ b/core/modules/media/js/media_type_form.js @@ -0,0 +1,46 @@ +/** + * @file + * Javascript for the media type form. + */ + +(function ($, Drupal) { + + 'use strict'; + + /** + * Behaviors for setting summaries on media type form. + * + * @type {Drupal~behavior} + * + * @prop {Drupal~behaviorAttach} attach + * Attaches summary behaviors on media type edit forms. + */ + Drupal.behaviors.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 0000000..8de1c64 --- /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_REPLACE); + } +} + +/** + * 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 = [ + '#type' => 'inline_template', + '#template' => '{{ error }} {{ description }}', + '#context' => [ + 'error' => $error, + 'description' => $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 0000000..eecaf8e --- /dev/null +++ b/core/modules/media/media.libraries.yml @@ -0,0 +1,13 @@ +media_form: + version: VERSION + js: + js/media_form.js: {} + dependencies: + - core/drupal.form + +media_type_form: + version: VERSION + js: + js/media_type_form.js: {} + dependencies: + - core/drupal.form diff --git a/core/modules/media/media.links.action.yml b/core/modules/media/media.links.action.yml new file mode 100644 index 0000000..a056d35 --- /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 0000000..1945ef5 --- /dev/null +++ b/core/modules/media/media.links.contextual.yml @@ -0,0 +1,10 @@ +entity.media.edit_form: + route_name: entity.media.edit_form + group: media + title: Edit + +entity.media.delete_form: + route_name: entity.media.delete_form + group: media + title: Delete + weight: 10 diff --git a/core/modules/media/media.links.menu.yml b/core/modules/media/media.links.menu.yml new file mode 100644 index 0000000..33dd679 --- /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 0000000..c7a669c --- /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 0000000..be48ec9 --- /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 .= '{{ 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 0000000..874a4ec --- /dev/null +++ b/core/modules/media/src/MediaAccessControlHandler.php @@ -0,0 +1,52 @@ +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 0000000..89c178f --- /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()) { + unset($element['unpublish']['#button_type']); + } + // Otherwise, the 'unpublish' button is primary and should come first. + else { + unset($element['publish']['#button_type']); + $element['unpublish']['#weight'] = -10; + } + + // Remove the "Save" button. + $element['submit']['#access'] = FALSE; + + $element['delete']['#access'] = $media->access('delete'); + $element['delete']['#weight'] = 100; + + return $element; + } + + /** + * Entity builder updating the media status with the submitted value. + * + * @param string $entity_type_id + * The entity type identifier. + * @param \Drupal\media\MediaInterface $media + * The media updated with the submitted values. + * @param array $form + * The complete form array. + * @param \Drupal\Core\Form\FormStateInterface $form_state + * The current state of the form. + * + * @see \Drupal\media\MediaForm::form() + */ + public function updateStatus($entity_type_id, MediaInterface $media, array $form, FormStateInterface $form_state) { + $element = $form_state->getTriggeringElement(); + if (!empty($element['#published_status'])) { + $media->setPublished(); + } + else { + $media->setUnpublished(); + } + } + + /** + * {@inheritdoc} + */ + public function save(array $form, FormStateInterface $form_state) { + $saved = parent::save($form, $form_state); + $context = ['@type' => $this->entity->bundle(), '%label' => $this->entity->label()]; + $logger = $this->logger('media'); + $t_args = ['@type' => $this->entity->bundle->entity->label(), '%label' => $this->entity->label()]; + + if ($saved === SAVED_NEW) { + $logger->notice('@type: added %label.', $context); + drupal_set_message($this->t('@type %label has been created.', $t_args)); + } + else { + $logger->notice('@type: updated %label.', $context); + drupal_set_message($this->t('@type %label has been updated.', $t_args)); + } + + $form_state->setRedirectUrl($this->entity->toUrl('canonical')); + return $saved; + } + +} diff --git a/core/modules/media/src/MediaInterface.php b/core/modules/media/src/MediaInterface.php new file mode 100644 index 0000000..5984a66 --- /dev/null +++ b/core/modules/media/src/MediaInterface.php @@ -0,0 +1,43 @@ +entityTypeManager = $entity_type_manager; + $this->entityFieldManager = $entity_field_manager; + $this->configFactory = $config_factory; + $this->fieldTypeManager = $field_type_manager; + $this->setConfiguration($configuration); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity_type.manager'), + $container->get('entity_field.manager'), + $container->get('config.factory'), + $container->get('plugin.manager.field.field_type') + ); + } + + /** + * {@inheritdoc} + */ + public function setConfiguration(array $configuration) { + $this->configuration = NestedArray::mergeDeep( + $this->defaultConfiguration(), + $configuration + ); + } + + /** + * {@inheritdoc} + */ + public function getConfiguration() { + return $this->configuration; + } + + /** + * {@inheritdoc} + */ + public function defaultConfiguration() { + return [ + 'source_field' => '', + ]; + } + + /** + * {@inheritdoc} + */ + public function shouldUpdateThumbnail(MediaInterface $media, $is_new = FALSE) { + $source_field_name = $this->configuration['source_field']; + + // Update thumbnail if we don't have a thumbnail yet, for new items, + // or if the value of the source field changed. + if (!$media->get('thumbnail')->entity || $is_new || (isset($media->original) && $media->get($source_field_name)->getValue() != $media->original->get($source_field_name)->getValue())) { + return TRUE; + } + return FALSE; + } + + /** + * {@inheritdoc} + */ + public function getDefaultThumbnail() { + return $this->configFactory->get('media.settings')->get('icon_base_uri') . '/generic.png'; + } + + /** + * {@inheritdoc} + */ + public function attachConstraints(MediaInterface $media) { + } + + /** + * {@inheritdoc} + */ + public function calculateDependencies() { + return []; + } + + /** + * {@inheritdoc} + */ + public function buildConfigurationForm(array $form, FormStateInterface $form_state) { + // If there are existing fields to choose from, allow the user to reuse one. + $options = []; + foreach ($this->entityFieldManager->getFieldStorageDefinitions('media') as $field_name => $field) { + $allowed_type = in_array($field->getType(), $this->pluginDefinition['allowed_field_types'], TRUE); + if ($allowed_type && !$field->isBaseField()) { + $options[$field_name] = $field->getLabel(); + } + } + + $form['source_field'] = [ + '#type' => 'select', + '#title' => $this->t('Field with source information'), + '#default_value' => $this->configuration['source_field'], + '#empty_option' => $this->t('- Create -'), + '#options' => $options, + '#description' => $this->t('Select the field that will store essential information about the media item. If "Create" is selected a new field will be automatically created.'), + ]; + + if (!$options && $form_state->get('operation') === 'add') { + $form['source_field']['#access'] = FALSE; + $field_definition = $this->fieldTypeManager->getDefinition(reset($this->pluginDefinition['allowed_field_types'])); + $form['source_field_message'] = [ + '#markup' => $this->t('%field_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 ($form_state->getValues() as $config_key => $config_value) { + if (isset($this->configuration[$config_key])) { + $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(); + } + } + + /** + * {@inheritdoc} + */ + public function getDefaultName(MediaInterface $media) { + return 'media:' . $media->bundle() . ':' . $media->uuid(); + } + + /** + * 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']), + ]); + } + + /** + * 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/MediaSourceInterface.php b/core/modules/media/src/MediaSourceInterface.php new file mode 100644 index 0000000..d9f12c1 --- /dev/null +++ b/core/modules/media/src/MediaSourceInterface.php @@ -0,0 +1,196 @@ +alterInfo('media_source_info'); + $this->setCacheBackend($cache_backend, 'media_source_plugins'); + } + +} diff --git a/core/modules/media/src/MediaThumbnailHandler.php b/core/modules/media/src/MediaThumbnailHandler.php new file mode 100644 index 0000000..74c3a83 --- /dev/null +++ b/core/modules/media/src/MediaThumbnailHandler.php @@ -0,0 +1,95 @@ +fileStorage = $entity_type_manager->getStorage('file'); + $this->stringTranslation = $translation; + } + + /** + * {@inheritdoc} + */ + public function updateThumbnail(MediaInterface $media, $from_queue = FALSE) { + $thumbnail_uri = $this->getThumbnailUri($media, $from_queue); + $existing = $this->fileStorage->getQuery() + ->condition('uri', $thumbnail_uri) + ->execute(); + + if ($existing) { + $media->thumbnail->target_id = reset($existing); + } + else { + /** @var \Drupal\file\FileInterface $file */ + $file = $this->fileStorage->create(['uri' => $thumbnail_uri]); + if ($owner = $media->getOwner()) { + $file->setOwner($owner); + } + $file->setPermanent(); + $file->save(); + $media->thumbnail->target_id = $file->id(); + } + + $media->thumbnail->alt = $this->t('Thumbnail'); + $media->thumbnail->title = $media->label(); + + return $media; + } + + /** + * 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 \Drupal\media\MediaInterface $media + * A media item. + * @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(MediaInterface $media, $from_queue) { + if ($media->bundle->entity->thumbnailDownloadsAreQueued() && $media->isNew()) { + $thumbnail_uri = $media->getSource()->getDefaultThumbnail(); + } + elseif ($media->bundle->entity->thumbnailDownloadsAreQueued() && !$from_queue) { + $thumbnail_uri = $media->get('thumbnail')->entity->getFileUri(); + } + else { + $thumbnail_uri = $media->getSource()->getThumbnail($media); + } + + return $thumbnail_uri; + } + +} diff --git a/core/modules/media/src/MediaThumbnailHandlerInterface.php b/core/modules/media/src/MediaThumbnailHandlerInterface.php new file mode 100644 index 0000000..01ecd9b --- /dev/null +++ b/core/modules/media/src/MediaThumbnailHandlerInterface.php @@ -0,0 +1,23 @@ +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 = ['_none' => $this->t('- Skip field -')]; + foreach ($this->entityFieldManager->getFieldDefinitions('media', $this->entity->id()) as $field_name => $field) { + if (!($field instanceof BaseFieldDefinition) || $field_name === 'name') { + $options[$field_name] = $field->getLabel(); + } + } + + $field_map = $this->entity->getFieldMap(); + foreach ($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] : '_none', + ]; + } + } + + $form['additional_settings'] = [ + '#type' => 'vertical_tabs', + '#attached' => [ + 'library' => ['media/media_type_form'], + ], + ]; + + $form['workflow'] = [ + '#type' => 'details', + '#title' => $this->t('Publishing options'), + '#group' => 'additional_settings', + ]; + + $form['workflow']['options'] = [ + '#type' => 'checkboxes', + '#title' => $this->t('Default options'), + '#default_value' => $this->getWorkflowOptions(), + '#options' => [ + 'status' => $this->t('Published'), + 'new_revision' => $this->t('Create new revision'), + 'queue_thumbnail_downloads' => $this->t('Queue thumbnail downloads'), + ], + ]; + + $form['workflow']['options']['status']['#description'] = $this->t('Media will be automatically published when created.'); + $form['workflow']['options']['new_revision']['#description'] = $this->t('Automatically create new revisions. Users with the Administer media permission will be able to override this option.'); + $form['workflow']['options']['queue_thumbnail_downloads']['#description'] = $this->t('Download thumbnails via a queue.'); + + if ($this->moduleHandler->moduleExists('language')) { + $form['language'] = [ + '#type' => 'details', + '#title' => $this->t('Language settings'), + '#group' => 'additional_settings', + ]; + + $language_configuration = 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 != '_none'; + } + )); + + parent::submitForm($form, $form_state); + + $this->entity->setQueueThumbnailDownloadsStatus((bool) $form_state->getValue(['options', 'queue_thumbnail_downloads'])); + $this->entity->setStatus((bool) $form_state->getValue(['options', 'status'])); + $this->entity->setNewRevision((bool) $form_state->getValue(['options', 'new_revision'])); + + if ($form['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->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, []); + + 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, []); + + 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 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 0000000..af775c9 --- /dev/null +++ b/core/modules/media/src/MediaTypeInterface.php @@ -0,0 +1,86 @@ +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 0000000..f186871 --- /dev/null +++ b/core/modules/media/src/MediaViewsData.php @@ -0,0 +1,24 @@ +currentUser = $current_user; + $this->tempStore = $temp_store_factory->get('media_multiple_delete_confirm'); + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('user.private_tempstore'), + $container->get('current_user') + ); + } + + /** + * {@inheritdoc} + */ + public function executeMultiple(array $entities) { + $info = []; + /** @var \Drupal\media\MediaInterface $media */ + foreach ($entities as $media) { + $langcode = $media->language()->getId(); + $info[$media->id()][$langcode] = $langcode; + } + $this->tempStore->set($this->currentUser->id(), $info); + } + + /** + * {@inheritdoc} + */ + public function execute($object = NULL) { + $this->executeMultiple($object ? [$object] : []); + } + + /** + * {@inheritdoc} + */ + public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { + /** @var \Drupal\media\MediaInterface $object */ + return $object->access('delete', $account, $return_as_object); + } + +} diff --git a/core/modules/media/src/Plugin/Action/PublishMedia.php b/core/modules/media/src/Plugin/Action/PublishMedia.php new file mode 100644 index 0000000..df19af4 --- /dev/null +++ b/core/modules/media/src/Plugin/Action/PublishMedia.php @@ -0,0 +1,38 @@ +setPublished()->save(); + } + + /** + * {@inheritdoc} + */ + public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { + /** @var \Drupal\media\MediaInterface $object */ + $result = $object->access('update', $account, TRUE) + ->andIf($object->status->access('update', $account, TRUE)); + + return $return_as_object ? $result : $result->isAllowed(); + } + +} diff --git a/core/modules/media/src/Plugin/Action/SaveMedia.php b/core/modules/media/src/Plugin/Action/SaveMedia.php new file mode 100644 index 0000000..4a20360 --- /dev/null +++ b/core/modules/media/src/Plugin/Action/SaveMedia.php @@ -0,0 +1,37 @@ +changed = 0; + $entity->save(); + } + + /** + * {@inheritdoc} + */ + public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { + /** @var \Drupal\media\MediaInterface $object */ + return $object->access('update', $account, $return_as_object); + } + +} diff --git a/core/modules/media/src/Plugin/Action/UnpublishMedia.php b/core/modules/media/src/Plugin/Action/UnpublishMedia.php new file mode 100644 index 0000000..53cd471 --- /dev/null +++ b/core/modules/media/src/Plugin/Action/UnpublishMedia.php @@ -0,0 +1,38 @@ +setUnpublished()->save(); + } + + /** + * {@inheritdoc} + */ + public function access($object, AccountInterface $account = NULL, $return_as_object = FALSE) { + /** @var \Drupal\media\MediaInterface $object */ + $result = $object->access('update', $account, TRUE) + ->andIf($object->status->access('update', $account, TRUE)); + + return $return_as_object ? $result : $result->isAllowed(); + } + +} diff --git a/core/modules/media/src/Plugin/Field/FieldFormatter/MediaThumbnailFormatter.php b/core/modules/media/src/Plugin/Field/FieldFormatter/MediaThumbnailFormatter.php new file mode 100644 index 0000000..f9a9556 --- /dev/null +++ b/core/modules/media/src/Plugin/Field/FieldFormatter/MediaThumbnailFormatter.php @@ -0,0 +1,190 @@ +renderer = $renderer; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $plugin_id, + $plugin_definition, + $configuration['field_definition'], + $configuration['settings'], + $configuration['label'], + $configuration['view_mode'], + $configuration['third_party_settings'], + $container->get('current_user'), + $container->get('entity_type.manager')->getStorage('image_style'), + $container->get('renderer') + ); + } + + /** + * {@inheritdoc} + * + * This has to be overridden because FileFormatterBase expects $item to be + * of type \Drupal\file\Plugin\Field\FieldType\FileItem and calls + * isDisplayed() which is not in FieldItemInterface. + */ + protected function needsEntityLoad(EntityReferenceItem $item) { + return !$item->hasNewEntity(); + } + + /** + * {@inheritdoc} + */ + public function settingsForm(array $form, FormStateInterface $form_state) { + $element = parent::settingsForm($form, $form_state); + + $link_types = [ + 'content' => $this->t('Content'), + 'media' => $this->t('Media entity'), + ]; + $element['image_link']['#options'] = $link_types; + + return $element; + } + + /** + * {@inheritdoc} + */ + public function settingsSummary() { + $summary = parent::settingsSummary(); + + $link_types = [ + 'content' => $this->t('Linked to content'), + 'media' => $this->t('Linked to media 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 = $this->getEntitiesToView($items, $langcode); + + // Early opt-out if the field is empty. + if (empty($media)) { + return $elements; + } + + $url = NULL; + $image_link_setting = $this->getSetting('image_link'); + // Check if the formatter involves a link. + if ($image_link_setting == 'content') { + $entity = $items->getEntity(); + if (!$entity->isNew()) { + $url = $entity->toUrl(); + } + } + elseif ($image_link_setting === 'media') { + $link_media = TRUE; + } + + $image_style_setting = $this->getSetting('image_style'); + + /** @var \Drupal\media\MediaInterface[] $media */ + foreach ($media as $delta => $media_item) { + if (isset($link_media)) { + $url = $media_item->toUrl(); + } + + $elements[$delta] = [ + '#theme' => 'image_formatter', + '#item' => $media_item->get('thumbnail'), + '#item_attributes' => [], + '#image_style' => $image_style_setting, + '#url' => $url, + ]; + + // Add cacheability of each item in the field. + $this->renderer->addCacheableDependency($elements[$delta], $media_item); + } + + // Add cacheability of the image style setting. + if ($image_link_setting && ($image_style = $this->imageStyleStorage->load($image_style_setting))) { + $this->renderer->addCacheableDependency($elements, $image_style); + } + + return $elements; + } + + /** + * {@inheritdoc} + */ + public static function isApplicable(FieldDefinitionInterface $field_definition) { + // This formatter is only available for entity types that reference + // media items. + $target_type = $field_definition->getFieldStorageDefinition()->getSetting('target_type'); + return $target_type == 'media'; + } + +} diff --git a/core/modules/media/src/Plugin/QueueWorker/ThumbnailDownloader.php b/core/modules/media/src/Plugin/QueueWorker/ThumbnailDownloader.php new file mode 100644 index 0000000..84f78e3 --- /dev/null +++ b/core/modules/media/src/Plugin/QueueWorker/ThumbnailDownloader.php @@ -0,0 +1,70 @@ +thumbnailHandler = $thumbnail_handler; + } + + /** + * {@inheritdoc} + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('media.thumbnail_handler') + ); + } + + /** + * {@inheritdoc} + */ + public function processItem($data) { + /** @var \Drupal\media\MediaInterface $entity */ + if ($entity = Media::load($data['id'])) { + $this->thumbnailHandler->updateThumbnail($entity, TRUE); + $entity->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 0000000..e77c21e --- /dev/null +++ b/core/modules/media/src/Plugin/views/wizard/Media.php @@ -0,0 +1,87 @@ + [ + 'value' => TRUE, + 'table' => 'media_field_data', + 'field' => 'status', + 'plugin_id' => 'boolean', + 'entity_type' => 'media', + 'entity_field' => 'status', + ], + ]; + + /** + * {@inheritdoc} + */ + public function getAvailableSorts() { + return [ + 'media_field_data-name:DESC' => $this->t('Media name'), + ]; + } + + /** + * {@inheritdoc} + */ + protected function defaultDisplayOptions() { + $display_options = parent::defaultDisplayOptions(); + + // Add permission-based access control. + $display_options['access']['type'] = 'perm'; + $display_options['access']['options']['perm'] = 'view media'; + + // Remove the default fields, since we are customizing them here. + unset($display_options['fields']); + + // Add the name field, so that the display has content if the user switches + // to a row style that uses fields. + $display_options['fields']['name']['id'] = 'name'; + $display_options['fields']['name']['table'] = 'media_field_data'; + $display_options['fields']['name']['field'] = 'name'; + $display_options['fields']['name']['entity_type'] = 'media'; + $display_options['fields']['name']['entity_field'] = 'media'; + $display_options['fields']['name']['label'] = ''; + $display_options['fields']['name']['alter']['alter_text'] = 0; + $display_options['fields']['name']['alter']['make_link'] = 0; + $display_options['fields']['name']['alter']['absolute'] = 0; + $display_options['fields']['name']['alter']['trim'] = 0; + $display_options['fields']['name']['alter']['word_boundary'] = 0; + $display_options['fields']['name']['alter']['ellipsis'] = 0; + $display_options['fields']['name']['alter']['strip_tags'] = 0; + $display_options['fields']['name']['alter']['html'] = 0; + $display_options['fields']['name']['hide_empty'] = 0; + $display_options['fields']['name']['empty_zero'] = 0; + $display_options['fields']['name']['settings']['link_to_entity'] = 1; + $display_options['fields']['name']['plugin_id'] = 'field'; + + return $display_options; + } + +} diff --git a/core/modules/media/src/Plugin/views/wizard/MediaRevision.php b/core/modules/media/src/Plugin/views/wizard/MediaRevision.php new file mode 100644 index 0000000..e0c256f --- /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/src/Tests/MediaCacheTagsTest.php b/core/modules/media/src/Tests/MediaCacheTagsTest.php new file mode 100644 index 0000000..226c3f0 --- /dev/null +++ b/core/modules/media/src/Tests/MediaCacheTagsTest.php @@ -0,0 +1,85 @@ +grantPermission('view media'); + $user_role->save(); + } + + /** + * {@inheritdoc} + */ + protected function createEntity() { + // Create a media type. + $id = strtolower($this->randomMachineName()); + MediaType::create([ + 'id' => $id, + 'label' => $id, + 'source' => 'test', + 'source_configuration' => [], + 'field_map' => [], + 'new_revision' => FALSE, + ])->save(); + + // Create a media item. + $media = Media::create([ + 'bundle' => $id, + 'name' => 'Unnamed', + ]); + $media->save(); + + return $media; + } + + /** + * {@inheritdoc} + */ + protected function getAdditionalCacheContextsForEntity(EntityInterface $media) { + return ['timezone']; + } + + /** + * {@inheritdoc} + * + * Each media item must have an author and a thumbnail. + */ + protected function getAdditionalCacheTagsForEntity(EntityInterface $media) { + return [ + 'user:' . $media->getOwnerId(), + 'config:image.style.thumbnail', + 'file:' . $media->get('thumbnail')->entity->id(), + ]; + } + +} diff --git a/core/modules/media/templates/media.html.twig b/core/modules/media/templates/media.html.twig new file mode 100644 index 0000000..baa97c3 --- /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 + */ +#} +