first commit
This commit is contained in:
@@ -0,0 +1,9 @@
|
||||
langcode: en
|
||||
status: false
|
||||
dependencies:
|
||||
module:
|
||||
- media
|
||||
id: media.full
|
||||
label: 'Full content'
|
||||
targetEntityType: media
|
||||
cache: true
|
||||
@@ -0,0 +1,4 @@
|
||||
icon_base_uri: 'public://media-icons/generic'
|
||||
iframe_domain: ''
|
||||
oembed_providers_url: 'https://oembed.com/providers.json'
|
||||
standalone_url: false
|
||||
@@ -0,0 +1,10 @@
|
||||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
module:
|
||||
- media
|
||||
id: media_delete_action
|
||||
label: 'Delete media'
|
||||
type: media
|
||||
plugin: entity:delete_action:media
|
||||
configuration: { }
|
||||
@@ -0,0 +1,10 @@
|
||||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
module:
|
||||
- media
|
||||
id: media_publish_action
|
||||
label: 'Publish media'
|
||||
type: media
|
||||
plugin: entity:publish_action:media
|
||||
configuration: { }
|
||||
@@ -0,0 +1,10 @@
|
||||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
module:
|
||||
- media
|
||||
id: media_save_action
|
||||
label: 'Save media'
|
||||
type: media
|
||||
plugin: entity:save_action:media
|
||||
configuration: { }
|
||||
@@ -0,0 +1,10 @@
|
||||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
module:
|
||||
- media
|
||||
id: media_unpublish_action
|
||||
label: 'Unpublish media'
|
||||
type: media
|
||||
plugin: entity:unpublish_action:media
|
||||
configuration: { }
|
||||
@@ -0,0 +1,904 @@
|
||||
langcode: en
|
||||
status: true
|
||||
dependencies:
|
||||
config:
|
||||
- image.style.thumbnail
|
||||
module:
|
||||
- image
|
||||
- media
|
||||
- user
|
||||
id: media
|
||||
label: Media
|
||||
module: views
|
||||
description: 'Find and manage media.'
|
||||
tag: ''
|
||||
base_table: media_field_data
|
||||
base_field: mid
|
||||
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: Filter
|
||||
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: true
|
||||
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: 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: Type
|
||||
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:
|
||||
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'
|
||||
operator_limit_selection: false
|
||||
operator_list: { }
|
||||
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
|
||||
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: Type
|
||||
description: ''
|
||||
use_operator: false
|
||||
operator: bundle_op
|
||||
identifier: type
|
||||
required: false
|
||||
remember: false
|
||||
multiple: false
|
||||
remember_roles:
|
||||
authenticated: authenticated
|
||||
anonymous: '0'
|
||||
administrator: '0'
|
||||
reduce: false
|
||||
operator_limit_selection: false
|
||||
operator_list: { }
|
||||
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
|
||||
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
|
||||
operator_limit_selection: false
|
||||
operator_list: { }
|
||||
is_grouped: true
|
||||
group_info:
|
||||
label: 'Published 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
|
||||
status_extra:
|
||||
id: status_extra
|
||||
table: media_field_data
|
||||
field: status_extra
|
||||
relationship: none
|
||||
group_type: group
|
||||
admin_label: ''
|
||||
operator: '='
|
||||
value: ''
|
||||
group: 1
|
||||
exposed: false
|
||||
expose:
|
||||
operator_id: ''
|
||||
label: ''
|
||||
description: ''
|
||||
use_operator: false
|
||||
operator: ''
|
||||
operator_limit_selection: false
|
||||
operator_list: { }
|
||||
identifier: ''
|
||||
required: false
|
||||
remember: false
|
||||
multiple: false
|
||||
remember_roles:
|
||||
authenticated: authenticated
|
||||
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
|
||||
plugin_id: media_status
|
||||
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
|
||||
operator_limit_selection: false
|
||||
operator_list: { }
|
||||
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 media 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
|
||||
- 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
|
||||
- user.permissions
|
||||
tags: { }
|
||||
@@ -0,0 +1,126 @@
|
||||
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'
|
||||
iframe_domain:
|
||||
type: uri
|
||||
label: 'Domain from which to serve oEmbed content in an iframe'
|
||||
oembed_providers_url:
|
||||
type: uri
|
||||
label: 'The URL of the oEmbed providers database in JSON format'
|
||||
standalone_url:
|
||||
type: boolean
|
||||
label: 'Allow media items to be viewed standalone at /media/{id}'
|
||||
|
||||
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
|
||||
|
||||
field.formatter.settings.media_thumbnail:
|
||||
type: field.formatter.settings.image
|
||||
label: 'Media thumbnail field display format settings'
|
||||
|
||||
field.formatter.settings.oembed:
|
||||
type: mapping
|
||||
label: 'oEmbed display format settings'
|
||||
mapping:
|
||||
max_width:
|
||||
type: integer
|
||||
label: 'Maximum width'
|
||||
max_height:
|
||||
type: integer
|
||||
label: 'Maximum height'
|
||||
|
||||
field.widget.settings.oembed_textfield:
|
||||
type: field.widget.settings.string_textfield
|
||||
label: 'oEmbed widget format settings'
|
||||
|
||||
media.source.*:
|
||||
type: mapping
|
||||
label: 'Media source settings'
|
||||
|
||||
media.source.file:
|
||||
type: media.source.field_aware
|
||||
label: '"File" media source configuration'
|
||||
|
||||
media.source.image:
|
||||
type: media.source.field_aware
|
||||
label: '"Image" media source configuration'
|
||||
|
||||
media.source.audio_file:
|
||||
type: media.source.field_aware
|
||||
label: '"Audio" media source configuration'
|
||||
|
||||
media.source.video_file:
|
||||
type: media.source.field_aware
|
||||
label: '"Video" media source configuration'
|
||||
|
||||
media.source.oembed:*:
|
||||
type: media.source.field_aware
|
||||
label: 'oEmbed media source configuration'
|
||||
mapping:
|
||||
thumbnails_directory:
|
||||
type: uri
|
||||
label: 'URI of thumbnail storage directory'
|
||||
providers:
|
||||
type: sequence
|
||||
label: 'Allowed oEmbed providers'
|
||||
sequence:
|
||||
type: string
|
||||
label: 'Provider name'
|
||||
|
||||
media.source.field_aware:
|
||||
type: mapping
|
||||
mapping:
|
||||
source_field:
|
||||
type: string
|
||||
label: 'Source field'
|
||||
|
||||
filter_settings.media_embed:
|
||||
type: filter
|
||||
label: 'Media Embed'
|
||||
mapping:
|
||||
default_view_mode:
|
||||
type: string
|
||||
label: 'The view mode that is used by default'
|
||||
allowed_media_types:
|
||||
type: sequence
|
||||
label: 'Media types selectable in the Media Library'
|
||||
sequence:
|
||||
type: string
|
||||
label: 'Media type'
|
||||
allowed_view_modes:
|
||||
type: sequence
|
||||
label: 'View modes selectable in the "Edit media" dialog'
|
||||
sequence:
|
||||
type: string
|
||||
label: 'View mode'
|
||||
@@ -0,0 +1,10 @@
|
||||
/**
|
||||
* @file
|
||||
* Caption filter: default styling for displaying Media Embed captions.
|
||||
*/
|
||||
|
||||
.caption .media .field,
|
||||
.caption .media .field * {
|
||||
float: none;
|
||||
margin: unset;
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
.media-oembed-content {
|
||||
max-width: 100%;
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
iframe {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
margin: 0;
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
/**
|
||||
* @file
|
||||
* Media embed: overrides to make focus styles and alignment work in CKEditor.
|
||||
*/
|
||||
|
||||
/**
|
||||
* Allow the drupal-media element's width to collapse to the size of its
|
||||
* contents so that the outline has no extra white space (margin). This
|
||||
* emulates the image2 plugin's styles inherited by the drupallink CKEditor
|
||||
* plugin.
|
||||
*/
|
||||
drupal-media {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
/**
|
||||
* For center alignment, take advantage of drupal-media's inline-block
|
||||
* display and center it as if it were text.
|
||||
*/
|
||||
.cke_widget_drupalmedia.align-center {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fix positioning without delete button. Can be removed with this issue:
|
||||
* @see https://www.drupal.org/project/drupal/issues/3074859
|
||||
*/
|
||||
drupal-media .media-library-item__edit {
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Allow alignment to display in CKEditor.
|
||||
*/
|
||||
drupal-media[data-align=left],
|
||||
drupal-media[data-align=right] {
|
||||
display: inline;
|
||||
}
|
||||
drupal-media[data-align=center] {
|
||||
display: flex;
|
||||
}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 5.2 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 7.1 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 4.1 KiB |
@@ -0,0 +1,39 @@
|
||||
/**
|
||||
* @file
|
||||
* Defines Javascript behaviors for the media form.
|
||||
*/
|
||||
|
||||
(function($, Drupal) {
|
||||
/**
|
||||
* 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(context) {
|
||||
const $context = $(context);
|
||||
|
||||
$context.find('.media-form-author').drupalSetSummary(context => {
|
||||
const $authorContext = $(context);
|
||||
const name = $authorContext.find('.field--name-uid input').val();
|
||||
const date = $authorContext.find('.field--name-created input').val();
|
||||
|
||||
if (name && date) {
|
||||
return Drupal.t('By @name on @date', {
|
||||
'@name': name,
|
||||
'@date': date,
|
||||
});
|
||||
}
|
||||
if (name) {
|
||||
return Drupal.t('By @name', { '@name': name });
|
||||
}
|
||||
if (date) {
|
||||
return Drupal.t('Authored on @date', { '@date': date });
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
})(jQuery, Drupal);
|
||||
@@ -0,0 +1,33 @@
|
||||
/**
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function ($, Drupal) {
|
||||
Drupal.behaviors.mediaFormSummaries = {
|
||||
attach: function attach(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
|
||||
});
|
||||
}
|
||||
if (name) {
|
||||
return Drupal.t('By @name', { '@name': name });
|
||||
}
|
||||
if (date) {
|
||||
return Drupal.t('Authored on @date', { '@date': date });
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
})(jQuery, Drupal);
|
||||
@@ -0,0 +1,30 @@
|
||||
/**
|
||||
* @file
|
||||
* Theme elements for the Media Embed CKEditor plugin.
|
||||
*/
|
||||
|
||||
(Drupal => {
|
||||
/**
|
||||
* Themes the error displayed when the media embed preview fails.
|
||||
*
|
||||
* @return {string}
|
||||
* A string representing a DOM fragment.
|
||||
*
|
||||
* @see media-embed-error.html.twig
|
||||
*/
|
||||
Drupal.theme.mediaEmbedPreviewError = () =>
|
||||
`<div>${Drupal.t(
|
||||
'An error occurred while trying to preview the media. Please save your work and reload this page.',
|
||||
)}</div>`;
|
||||
|
||||
/**
|
||||
* Themes the edit button for a media embed.
|
||||
*
|
||||
* @return {string}
|
||||
* An HTML string to insert in the CKEditor.
|
||||
*/
|
||||
Drupal.theme.mediaEmbedEditButton = () =>
|
||||
`<button class="media-library-item__edit">${Drupal.t(
|
||||
'Edit media',
|
||||
)}</button>`;
|
||||
})(Drupal);
|
||||
@@ -0,0 +1,16 @@
|
||||
/**
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function (Drupal) {
|
||||
Drupal.theme.mediaEmbedPreviewError = function () {
|
||||
return '<div>' + Drupal.t('An error occurred while trying to preview the media. Please save your work and reload this page.') + '</div>';
|
||||
};
|
||||
|
||||
Drupal.theme.mediaEmbedEditButton = function () {
|
||||
return '<button class="media-library-item__edit">' + Drupal.t('Edit media') + '</button>';
|
||||
};
|
||||
})(Drupal);
|
||||
@@ -0,0 +1,495 @@
|
||||
/**
|
||||
* @file
|
||||
* Drupal Media embed plugin.
|
||||
*/
|
||||
|
||||
(function(jQuery, Drupal, CKEDITOR) {
|
||||
/**
|
||||
* Gets the focused widget, if of the type specific for this plugin.
|
||||
*
|
||||
* @param {CKEDITOR.editor} editor
|
||||
* A CKEditor instance.
|
||||
*
|
||||
* @return {?CKEDITOR.plugins.widget}
|
||||
* The focused drupalmedia widget instance, or null.
|
||||
*/
|
||||
function getFocusedWidget(editor) {
|
||||
const widget = editor.widgets.focused;
|
||||
|
||||
if (widget && widget.name === 'drupalmedia') {
|
||||
return widget;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Makes embedded items linkable by integrating with the drupallink plugin.
|
||||
*
|
||||
* @param {CKEDITOR.editor} editor
|
||||
* A CKEditor instance.
|
||||
*/
|
||||
function linkCommandIntegrator(editor) {
|
||||
if (!editor.plugins.drupallink) {
|
||||
return;
|
||||
}
|
||||
|
||||
CKEDITOR.plugins.drupallink.registerLinkableWidget('drupalmedia');
|
||||
|
||||
editor.getCommand('drupalunlink').on('exec', function(evt) {
|
||||
const widget = getFocusedWidget(editor);
|
||||
|
||||
if (!widget) {
|
||||
return;
|
||||
}
|
||||
|
||||
widget.setData('link', null);
|
||||
|
||||
this.refresh(editor, editor.elementPath());
|
||||
|
||||
evt.cancel();
|
||||
});
|
||||
|
||||
editor.getCommand('drupalunlink').on('refresh', function(evt) {
|
||||
const widget = getFocusedWidget(editor);
|
||||
|
||||
if (!widget) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState(
|
||||
widget.data.link ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED,
|
||||
);
|
||||
|
||||
evt.cancel();
|
||||
});
|
||||
|
||||
// Register context menu items for editing link.
|
||||
if (editor.contextMenu) {
|
||||
editor.contextMenu.addListener(() => {
|
||||
const widget = getFocusedWidget(editor);
|
||||
|
||||
if (!widget) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (widget.data.link) {
|
||||
return {
|
||||
link: CKEDITOR.TRISTATE_OFF,
|
||||
unlink: CKEDITOR.TRISTATE_OFF,
|
||||
};
|
||||
}
|
||||
return {};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
CKEDITOR.plugins.add('drupalmedia', {
|
||||
requires: 'widget',
|
||||
|
||||
beforeInit(editor) {
|
||||
// Configure CKEditor DTD for custom drupal-media element.
|
||||
// @see https://www.drupal.org/node/2448449#comment-9717735
|
||||
const { dtd } = CKEDITOR;
|
||||
// Allow text within the drupal-media tag.
|
||||
dtd['drupal-media'] = { '#': 1 };
|
||||
// Register drupal-media element as an allowed child in each tag that can
|
||||
// contain a div element and as an allowed child of the a tag.
|
||||
Object.keys(dtd).forEach(tagName => {
|
||||
if (dtd[tagName].div) {
|
||||
dtd[tagName]['drupal-media'] = 1;
|
||||
}
|
||||
});
|
||||
dtd.a['drupal-media'] = 1;
|
||||
|
||||
editor.widgets.add('drupalmedia', {
|
||||
allowedContent: {
|
||||
'drupal-media': {
|
||||
attributes: {
|
||||
'!data-entity-type': true,
|
||||
'!data-entity-uuid': true,
|
||||
'data-align': true,
|
||||
'data-caption': true,
|
||||
alt: true,
|
||||
title: true,
|
||||
},
|
||||
classes: {},
|
||||
},
|
||||
},
|
||||
// Minimum HTML which is required by this widget to work.
|
||||
// This does not use the object format used above, but a
|
||||
// CKEDITOR.style instance, because requiredContent does not support
|
||||
// the object format.
|
||||
// @see https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR_filter_contentRule.html
|
||||
requiredContent: new CKEDITOR.style({
|
||||
element: 'drupal-media',
|
||||
attributes: {
|
||||
'data-entity-type': '',
|
||||
'data-entity-uuid': '',
|
||||
},
|
||||
}),
|
||||
|
||||
pathName: Drupal.t('Embedded media'),
|
||||
|
||||
editables: {
|
||||
caption: {
|
||||
selector: 'figcaption',
|
||||
allowedContent: 'a[!href]; em strong cite code br',
|
||||
pathName: Drupal.t('Caption'),
|
||||
},
|
||||
},
|
||||
|
||||
getLabel() {
|
||||
if (this.data.label) {
|
||||
return this.data.label;
|
||||
}
|
||||
return Drupal.t('Embedded media');
|
||||
},
|
||||
|
||||
upcast(element, data) {
|
||||
const { attributes } = element;
|
||||
// This matches the behavior of the corresponding server-side text filter plugin.
|
||||
if (
|
||||
element.name !== 'drupal-media' ||
|
||||
attributes['data-entity-type'] !== 'media' ||
|
||||
attributes['data-entity-uuid'] === undefined
|
||||
) {
|
||||
return;
|
||||
}
|
||||
data.attributes = CKEDITOR.tools.copy(attributes);
|
||||
data.hasCaption = data.attributes.hasOwnProperty('data-caption');
|
||||
// Add space to the empty caption to allow the server-side text
|
||||
// filter to render a caption, allowing the placeholder-rendering
|
||||
// CSS to work.
|
||||
if (data.hasCaption && data.attributes['data-caption'] === '') {
|
||||
data.attributes['data-caption'] = ' ';
|
||||
}
|
||||
data.label = null;
|
||||
data.link = null;
|
||||
if (element.parent.name === 'a') {
|
||||
data.link = CKEDITOR.tools.copy(element.parent.attributes);
|
||||
// Omit CKEditor-internal attributes.
|
||||
Object.keys(element.parent.attributes).forEach(attrName => {
|
||||
if (attrName.indexOf('data-cke-') !== -1) {
|
||||
delete data.link[attrName];
|
||||
}
|
||||
});
|
||||
}
|
||||
// @see media_field_widget_form_alter()
|
||||
const hostEntityLangcode = document
|
||||
.getElementById(editor.name)
|
||||
.getAttribute('data-media-embed-host-entity-langcode');
|
||||
if (hostEntityLangcode) {
|
||||
data.hostEntityLangcode = hostEntityLangcode;
|
||||
}
|
||||
return element;
|
||||
},
|
||||
|
||||
destroy() {
|
||||
this._tearDownDynamicEditables();
|
||||
},
|
||||
|
||||
data(event) {
|
||||
// Only run during changes.
|
||||
if (this.oldData) {
|
||||
// The server-side text filter plugin treats both an empty
|
||||
// `data-caption` attribute and a non-existing one the same: it
|
||||
// does not render a caption. But in the CKEditor Widget, we need
|
||||
// to be able to show an empty caption with placeholder text using
|
||||
// CSS even when technically there is no `data-caption` attribute
|
||||
// value yet. That's why this CKEditor Widget has an independent
|
||||
// `hasCaption` boolean (which is not an attribute) to know when
|
||||
// to generate a non-empty `data-caption` attribute when the
|
||||
// content creator has enabled caption: this makes the server-side
|
||||
// text filter render a caption, allowing the placeholder-rendering
|
||||
// CSS to work.
|
||||
// @see core/modules/filter/css/filter.caption.css
|
||||
// @see ckeditor_ckeditor_css_alter()
|
||||
if (!this.data.hasCaption && this.oldData.hasCaption) {
|
||||
delete this.data.attributes['data-caption'];
|
||||
} else if (
|
||||
this.data.hasCaption &&
|
||||
!this.data.attributes['data-caption']
|
||||
) {
|
||||
this.data.attributes['data-caption'] = ' ';
|
||||
}
|
||||
}
|
||||
|
||||
if (this._previewNeedsServerSideUpdate()) {
|
||||
editor.fire('lockSnapshot');
|
||||
this._tearDownDynamicEditables();
|
||||
|
||||
this._loadPreview(widget => {
|
||||
widget._setUpDynamicEditables();
|
||||
widget._setUpEditButton();
|
||||
editor.fire('unlockSnapshot');
|
||||
});
|
||||
}
|
||||
|
||||
// Remove old attributes from drupal-media element within the widget.
|
||||
if (this.oldData) {
|
||||
Object.keys(this.oldData.attributes).forEach(attrName => {
|
||||
this.element.removeAttribute(attrName);
|
||||
});
|
||||
}
|
||||
// Add attributes to drupal-media element within the widget.
|
||||
this.element.setAttributes(this.data.attributes);
|
||||
|
||||
// Track the previous state to allow checking if preview needs
|
||||
// server side update.
|
||||
this.oldData = CKEDITOR.tools.clone(this.data);
|
||||
},
|
||||
|
||||
downcast() {
|
||||
const downcastElement = new CKEDITOR.htmlParser.element(
|
||||
'drupal-media',
|
||||
this.data.attributes,
|
||||
);
|
||||
if (this.data.link) {
|
||||
const link = new CKEDITOR.htmlParser.element('a', this.data.link);
|
||||
link.add(downcastElement);
|
||||
return link;
|
||||
}
|
||||
return downcastElement;
|
||||
},
|
||||
|
||||
_setUpDynamicEditables() {
|
||||
// Now that the caption is available in the DOM, make it editable.
|
||||
if (this.initEditable('caption', this.definition.editables.caption)) {
|
||||
const captionEditable = this.editables.caption;
|
||||
// @see core/modules/filter/css/filter.caption.css
|
||||
// @see ckeditor_ckeditor_css_alter()
|
||||
captionEditable.setAttribute(
|
||||
'data-placeholder',
|
||||
Drupal.t('Enter caption here'),
|
||||
);
|
||||
// Ensure that any changes made to the caption are persisted in the
|
||||
// widget's data-caption attribute.
|
||||
this.captionObserver = new MutationObserver(() => {
|
||||
const mediaAttributes = CKEDITOR.tools.clone(
|
||||
this.data.attributes,
|
||||
);
|
||||
mediaAttributes['data-caption'] = captionEditable.getData();
|
||||
this.setData('attributes', mediaAttributes);
|
||||
});
|
||||
this.captionObserver.observe(captionEditable.$, {
|
||||
characterData: true,
|
||||
attributes: true,
|
||||
childList: true,
|
||||
subtree: true,
|
||||
});
|
||||
// Some browsers will add a <br> tag to a newly created DOM element
|
||||
// with no content. Remove this <br> if it is the only thing in the
|
||||
// caption. Our placeholder support requires the element to be
|
||||
// entirely empty.
|
||||
// @see core/modules/filter/css/filter.caption.css
|
||||
// @see core/modules/ckeditor/js/plugins/drupalimagecaption/plugin.es6.js
|
||||
if (
|
||||
captionEditable.$.childNodes.length === 1 &&
|
||||
captionEditable.$.childNodes.item(0).nodeName === 'BR'
|
||||
) {
|
||||
captionEditable.$.removeChild(
|
||||
captionEditable.$.childNodes.item(0),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Injects HTML for edit button into the preview that was just loaded.
|
||||
*/
|
||||
_setUpEditButton() {
|
||||
// No buttons for missing media.
|
||||
if (this.element.findOne('.media-embed-error')) {
|
||||
return;
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if a node is an element node.
|
||||
*
|
||||
* @param {CKEDITOR.dom.node} n
|
||||
* A DOM node to evaluate.
|
||||
*
|
||||
* @return {bool}
|
||||
* Returns true if node is an element node and not a non-element
|
||||
* node (such as NODE_TEXT, NODE_COMMENT, NODE_DOCUMENT or
|
||||
* NODE_DOCUMENT_FRAGMENT).
|
||||
*
|
||||
* @see https://ckeditor.com/docs/ckeditor4/latest/api/CKEDITOR.html#property-NODE_ELEMENT
|
||||
*/
|
||||
const isElementNode = function(n) {
|
||||
return n.type === CKEDITOR.NODE_ELEMENT;
|
||||
};
|
||||
|
||||
// Find the actual embedded media in the DOM.
|
||||
const embeddedMediaContainer = this.data.hasCaption
|
||||
? this.element.findOne('figure')
|
||||
: this.element;
|
||||
let embeddedMedia = embeddedMediaContainer.getFirst(isElementNode);
|
||||
// If there is a link, the top-level element is the `a` tag, and the
|
||||
// embedded media will be within the `a` tag.
|
||||
if (this.data.link) {
|
||||
embeddedMedia = embeddedMedia.getFirst(isElementNode);
|
||||
}
|
||||
// To allow the edit button to be absolutely positioned, the parent
|
||||
// element must be positioned relative.
|
||||
embeddedMedia.setStyle('position', 'relative');
|
||||
|
||||
const editButton = CKEDITOR.dom.element.createFromHtml(
|
||||
Drupal.theme('mediaEmbedEditButton'),
|
||||
);
|
||||
embeddedMedia.getFirst().insertBeforeMe(editButton);
|
||||
|
||||
// Make the edit button do things.
|
||||
const widget = this;
|
||||
this.element
|
||||
.findOne('.media-library-item__edit')
|
||||
.on('click', event => {
|
||||
const saveCallback = function(values) {
|
||||
event.cancel();
|
||||
editor.fire('saveSnapshot');
|
||||
if (values.hasOwnProperty('attributes')) {
|
||||
// Combine the dialog attributes with the widget attributes.
|
||||
// This copies the properties from widget.data.attributes to
|
||||
// values.attributes. (Properties already present
|
||||
// in values.attributes are not overwritten.)
|
||||
CKEDITOR.tools.extend(
|
||||
values.attributes,
|
||||
widget.data.attributes,
|
||||
);
|
||||
// Allow the dialog to delete attributes by setting them
|
||||
// to `false` or `none`. For example: `alt`.
|
||||
Object.keys(values.attributes).forEach(prop => {
|
||||
if (
|
||||
values.attributes[prop] === false ||
|
||||
(prop === 'data-align' &&
|
||||
values.attributes[prop] === 'none')
|
||||
) {
|
||||
delete values.attributes[prop];
|
||||
}
|
||||
});
|
||||
}
|
||||
widget.setData({
|
||||
attributes: values.attributes,
|
||||
hasCaption: !!values.hasCaption,
|
||||
});
|
||||
editor.fire('saveSnapshot');
|
||||
};
|
||||
|
||||
Drupal.ckeditor.openDialog(
|
||||
editor,
|
||||
Drupal.url(
|
||||
`editor/dialog/media/${editor.config.drupal.format}`,
|
||||
),
|
||||
widget.data,
|
||||
saveCallback,
|
||||
{},
|
||||
);
|
||||
});
|
||||
|
||||
// Allow opening the dialog with the return key or the space bar
|
||||
// by triggering a click event when a keydown event occurs on
|
||||
// the edit button.
|
||||
this.element
|
||||
.findOne('.media-library-item__edit')
|
||||
.on('keydown', event => {
|
||||
// The character code for the return key.
|
||||
const returnKey = 13;
|
||||
// The character code for the space bar.
|
||||
const spaceBar = 32;
|
||||
if (typeof event.data !== 'undefined') {
|
||||
const keypress = event.data.getKey();
|
||||
if (keypress === returnKey || keypress === spaceBar) {
|
||||
// Clicks the edit button that triggered the 'keydown'
|
||||
// event.
|
||||
event.sender.$.click();
|
||||
}
|
||||
// Stop propagation to keep the return key from
|
||||
// adding a line break.
|
||||
event.data.$.stopPropagation();
|
||||
event.data.$.stopImmediatePropagation();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_tearDownDynamicEditables() {
|
||||
// If we are watching for changes to the caption, stop doing that.
|
||||
if (this.captionObserver) {
|
||||
this.captionObserver.disconnect();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Determines if the preview needs to be re-rendered by the server.
|
||||
*
|
||||
* @return {boolean}
|
||||
* Returns true if the data hashes differ.
|
||||
*/
|
||||
_previewNeedsServerSideUpdate() {
|
||||
// When the widget is first loading, it of course needs to still get a preview!
|
||||
if (!this.ready) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this._hashData(this.oldData) !== this._hashData(this.data);
|
||||
},
|
||||
|
||||
/**
|
||||
* Computes a hash of the data that can only be previewed by the server.
|
||||
*
|
||||
* @return {string}
|
||||
*/
|
||||
_hashData(data) {
|
||||
const dataToHash = CKEDITOR.tools.clone(data);
|
||||
// The caption does not need rendering.
|
||||
delete dataToHash.attributes['data-caption'];
|
||||
// The media entity's label is server-side data and cannot be
|
||||
// modified by the content author.
|
||||
delete dataToHash.label;
|
||||
// Changed link destinations do not affect the visual preview.
|
||||
if (dataToHash.link) {
|
||||
delete dataToHash.link.href;
|
||||
}
|
||||
return JSON.stringify(dataToHash);
|
||||
},
|
||||
|
||||
/**
|
||||
* Loads an media embed preview and runs a callback after insertion.
|
||||
*
|
||||
* Note the absence of caching, that's because this uses a GET request (which is cacheable) and the server takes
|
||||
* special care to make the responses privately cacheable (i.e. per session) in the browser.
|
||||
*
|
||||
* @see \Drupal\media\Controller\MediaFilterController::preview()
|
||||
*
|
||||
* @param {function} callback
|
||||
* A callback function that will be called after the preview has
|
||||
* loaded. Receives the widget instance.
|
||||
*/
|
||||
_loadPreview(callback) {
|
||||
jQuery.get({
|
||||
url: Drupal.url(`media/${editor.config.drupal.format}/preview`),
|
||||
data: {
|
||||
text: this.downcast().getOuterHtml(),
|
||||
uuid: this.data.attributes['data-entity-uuid'],
|
||||
},
|
||||
dataType: 'html',
|
||||
success: (previewHtml, textStatus, jqXhr) => {
|
||||
this.element.setHtml(previewHtml);
|
||||
this.setData(
|
||||
'label',
|
||||
jqXhr.getResponseHeader('Drupal-Media-Label'),
|
||||
);
|
||||
callback(this);
|
||||
},
|
||||
error: () => {
|
||||
this.element.setHtml(Drupal.theme('mediaEmbedPreviewError'));
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
afterInit(editor) {
|
||||
linkCommandIntegrator(editor);
|
||||
},
|
||||
});
|
||||
})(jQuery, Drupal, CKEDITOR);
|
||||
@@ -0,0 +1,333 @@
|
||||
/**
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function (jQuery, Drupal, CKEDITOR) {
|
||||
function getFocusedWidget(editor) {
|
||||
var widget = editor.widgets.focused;
|
||||
|
||||
if (widget && widget.name === 'drupalmedia') {
|
||||
return widget;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function linkCommandIntegrator(editor) {
|
||||
if (!editor.plugins.drupallink) {
|
||||
return;
|
||||
}
|
||||
|
||||
CKEDITOR.plugins.drupallink.registerLinkableWidget('drupalmedia');
|
||||
|
||||
editor.getCommand('drupalunlink').on('exec', function (evt) {
|
||||
var widget = getFocusedWidget(editor);
|
||||
|
||||
if (!widget) {
|
||||
return;
|
||||
}
|
||||
|
||||
widget.setData('link', null);
|
||||
|
||||
this.refresh(editor, editor.elementPath());
|
||||
|
||||
evt.cancel();
|
||||
});
|
||||
|
||||
editor.getCommand('drupalunlink').on('refresh', function (evt) {
|
||||
var widget = getFocusedWidget(editor);
|
||||
|
||||
if (!widget) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.setState(widget.data.link ? CKEDITOR.TRISTATE_OFF : CKEDITOR.TRISTATE_DISABLED);
|
||||
|
||||
evt.cancel();
|
||||
});
|
||||
|
||||
if (editor.contextMenu) {
|
||||
editor.contextMenu.addListener(function () {
|
||||
var widget = getFocusedWidget(editor);
|
||||
|
||||
if (!widget) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (widget.data.link) {
|
||||
return {
|
||||
link: CKEDITOR.TRISTATE_OFF,
|
||||
unlink: CKEDITOR.TRISTATE_OFF
|
||||
};
|
||||
}
|
||||
return {};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
CKEDITOR.plugins.add('drupalmedia', {
|
||||
requires: 'widget',
|
||||
|
||||
beforeInit: function beforeInit(editor) {
|
||||
var dtd = CKEDITOR.dtd;
|
||||
|
||||
dtd['drupal-media'] = { '#': 1 };
|
||||
|
||||
Object.keys(dtd).forEach(function (tagName) {
|
||||
if (dtd[tagName].div) {
|
||||
dtd[tagName]['drupal-media'] = 1;
|
||||
}
|
||||
});
|
||||
dtd.a['drupal-media'] = 1;
|
||||
|
||||
editor.widgets.add('drupalmedia', {
|
||||
allowedContent: {
|
||||
'drupal-media': {
|
||||
attributes: {
|
||||
'!data-entity-type': true,
|
||||
'!data-entity-uuid': true,
|
||||
'data-align': true,
|
||||
'data-caption': true,
|
||||
alt: true,
|
||||
title: true
|
||||
},
|
||||
classes: {}
|
||||
}
|
||||
},
|
||||
|
||||
requiredContent: new CKEDITOR.style({
|
||||
element: 'drupal-media',
|
||||
attributes: {
|
||||
'data-entity-type': '',
|
||||
'data-entity-uuid': ''
|
||||
}
|
||||
}),
|
||||
|
||||
pathName: Drupal.t('Embedded media'),
|
||||
|
||||
editables: {
|
||||
caption: {
|
||||
selector: 'figcaption',
|
||||
allowedContent: 'a[!href]; em strong cite code br',
|
||||
pathName: Drupal.t('Caption')
|
||||
}
|
||||
},
|
||||
|
||||
getLabel: function getLabel() {
|
||||
if (this.data.label) {
|
||||
return this.data.label;
|
||||
}
|
||||
return Drupal.t('Embedded media');
|
||||
},
|
||||
upcast: function upcast(element, data) {
|
||||
var attributes = element.attributes;
|
||||
|
||||
if (element.name !== 'drupal-media' || attributes['data-entity-type'] !== 'media' || attributes['data-entity-uuid'] === undefined) {
|
||||
return;
|
||||
}
|
||||
data.attributes = CKEDITOR.tools.copy(attributes);
|
||||
data.hasCaption = data.attributes.hasOwnProperty('data-caption');
|
||||
|
||||
if (data.hasCaption && data.attributes['data-caption'] === '') {
|
||||
data.attributes['data-caption'] = ' ';
|
||||
}
|
||||
data.label = null;
|
||||
data.link = null;
|
||||
if (element.parent.name === 'a') {
|
||||
data.link = CKEDITOR.tools.copy(element.parent.attributes);
|
||||
|
||||
Object.keys(element.parent.attributes).forEach(function (attrName) {
|
||||
if (attrName.indexOf('data-cke-') !== -1) {
|
||||
delete data.link[attrName];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
var hostEntityLangcode = document.getElementById(editor.name).getAttribute('data-media-embed-host-entity-langcode');
|
||||
if (hostEntityLangcode) {
|
||||
data.hostEntityLangcode = hostEntityLangcode;
|
||||
}
|
||||
return element;
|
||||
},
|
||||
destroy: function destroy() {
|
||||
this._tearDownDynamicEditables();
|
||||
},
|
||||
data: function data(event) {
|
||||
var _this = this;
|
||||
|
||||
if (this.oldData) {
|
||||
if (!this.data.hasCaption && this.oldData.hasCaption) {
|
||||
delete this.data.attributes['data-caption'];
|
||||
} else if (this.data.hasCaption && !this.data.attributes['data-caption']) {
|
||||
this.data.attributes['data-caption'] = ' ';
|
||||
}
|
||||
}
|
||||
|
||||
if (this._previewNeedsServerSideUpdate()) {
|
||||
editor.fire('lockSnapshot');
|
||||
this._tearDownDynamicEditables();
|
||||
|
||||
this._loadPreview(function (widget) {
|
||||
widget._setUpDynamicEditables();
|
||||
widget._setUpEditButton();
|
||||
editor.fire('unlockSnapshot');
|
||||
});
|
||||
}
|
||||
|
||||
if (this.oldData) {
|
||||
Object.keys(this.oldData.attributes).forEach(function (attrName) {
|
||||
_this.element.removeAttribute(attrName);
|
||||
});
|
||||
}
|
||||
|
||||
this.element.setAttributes(this.data.attributes);
|
||||
|
||||
this.oldData = CKEDITOR.tools.clone(this.data);
|
||||
},
|
||||
downcast: function downcast() {
|
||||
var downcastElement = new CKEDITOR.htmlParser.element('drupal-media', this.data.attributes);
|
||||
if (this.data.link) {
|
||||
var link = new CKEDITOR.htmlParser.element('a', this.data.link);
|
||||
link.add(downcastElement);
|
||||
return link;
|
||||
}
|
||||
return downcastElement;
|
||||
},
|
||||
_setUpDynamicEditables: function _setUpDynamicEditables() {
|
||||
var _this2 = this;
|
||||
|
||||
if (this.initEditable('caption', this.definition.editables.caption)) {
|
||||
var captionEditable = this.editables.caption;
|
||||
|
||||
captionEditable.setAttribute('data-placeholder', Drupal.t('Enter caption here'));
|
||||
|
||||
this.captionObserver = new MutationObserver(function () {
|
||||
var mediaAttributes = CKEDITOR.tools.clone(_this2.data.attributes);
|
||||
mediaAttributes['data-caption'] = captionEditable.getData();
|
||||
_this2.setData('attributes', mediaAttributes);
|
||||
});
|
||||
this.captionObserver.observe(captionEditable.$, {
|
||||
characterData: true,
|
||||
attributes: true,
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
|
||||
if (captionEditable.$.childNodes.length === 1 && captionEditable.$.childNodes.item(0).nodeName === 'BR') {
|
||||
captionEditable.$.removeChild(captionEditable.$.childNodes.item(0));
|
||||
}
|
||||
}
|
||||
},
|
||||
_setUpEditButton: function _setUpEditButton() {
|
||||
if (this.element.findOne('.media-embed-error')) {
|
||||
return;
|
||||
}
|
||||
|
||||
var isElementNode = function isElementNode(n) {
|
||||
return n.type === CKEDITOR.NODE_ELEMENT;
|
||||
};
|
||||
|
||||
var embeddedMediaContainer = this.data.hasCaption ? this.element.findOne('figure') : this.element;
|
||||
var embeddedMedia = embeddedMediaContainer.getFirst(isElementNode);
|
||||
|
||||
if (this.data.link) {
|
||||
embeddedMedia = embeddedMedia.getFirst(isElementNode);
|
||||
}
|
||||
|
||||
embeddedMedia.setStyle('position', 'relative');
|
||||
|
||||
var editButton = CKEDITOR.dom.element.createFromHtml(Drupal.theme('mediaEmbedEditButton'));
|
||||
embeddedMedia.getFirst().insertBeforeMe(editButton);
|
||||
|
||||
var widget = this;
|
||||
this.element.findOne('.media-library-item__edit').on('click', function (event) {
|
||||
var saveCallback = function saveCallback(values) {
|
||||
event.cancel();
|
||||
editor.fire('saveSnapshot');
|
||||
if (values.hasOwnProperty('attributes')) {
|
||||
CKEDITOR.tools.extend(values.attributes, widget.data.attributes);
|
||||
|
||||
Object.keys(values.attributes).forEach(function (prop) {
|
||||
if (values.attributes[prop] === false || prop === 'data-align' && values.attributes[prop] === 'none') {
|
||||
delete values.attributes[prop];
|
||||
}
|
||||
});
|
||||
}
|
||||
widget.setData({
|
||||
attributes: values.attributes,
|
||||
hasCaption: !!values.hasCaption
|
||||
});
|
||||
editor.fire('saveSnapshot');
|
||||
};
|
||||
|
||||
Drupal.ckeditor.openDialog(editor, Drupal.url('editor/dialog/media/' + editor.config.drupal.format), widget.data, saveCallback, {});
|
||||
});
|
||||
|
||||
this.element.findOne('.media-library-item__edit').on('keydown', function (event) {
|
||||
var returnKey = 13;
|
||||
|
||||
var spaceBar = 32;
|
||||
if (typeof event.data !== 'undefined') {
|
||||
var keypress = event.data.getKey();
|
||||
if (keypress === returnKey || keypress === spaceBar) {
|
||||
event.sender.$.click();
|
||||
}
|
||||
|
||||
event.data.$.stopPropagation();
|
||||
event.data.$.stopImmediatePropagation();
|
||||
}
|
||||
});
|
||||
},
|
||||
_tearDownDynamicEditables: function _tearDownDynamicEditables() {
|
||||
if (this.captionObserver) {
|
||||
this.captionObserver.disconnect();
|
||||
}
|
||||
},
|
||||
_previewNeedsServerSideUpdate: function _previewNeedsServerSideUpdate() {
|
||||
if (!this.ready) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return this._hashData(this.oldData) !== this._hashData(this.data);
|
||||
},
|
||||
_hashData: function _hashData(data) {
|
||||
var dataToHash = CKEDITOR.tools.clone(data);
|
||||
|
||||
delete dataToHash.attributes['data-caption'];
|
||||
|
||||
delete dataToHash.label;
|
||||
|
||||
if (dataToHash.link) {
|
||||
delete dataToHash.link.href;
|
||||
}
|
||||
return JSON.stringify(dataToHash);
|
||||
},
|
||||
_loadPreview: function _loadPreview(callback) {
|
||||
var _this3 = this;
|
||||
|
||||
jQuery.get({
|
||||
url: Drupal.url('media/' + editor.config.drupal.format + '/preview'),
|
||||
data: {
|
||||
text: this.downcast().getOuterHtml(),
|
||||
uuid: this.data.attributes['data-entity-uuid']
|
||||
},
|
||||
dataType: 'html',
|
||||
success: function success(previewHtml, textStatus, jqXhr) {
|
||||
_this3.element.setHtml(previewHtml);
|
||||
_this3.setData('label', jqXhr.getResponseHeader('Drupal-Media-Label'));
|
||||
callback(_this3);
|
||||
},
|
||||
error: function error() {
|
||||
_this3.element.setHtml(Drupal.theme('mediaEmbedPreviewError'));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
afterInit: function afterInit(editor) {
|
||||
linkCommandIntegrator(editor);
|
||||
}
|
||||
});
|
||||
})(jQuery, Drupal, CKEDITOR);
|
||||
@@ -0,0 +1,66 @@
|
||||
/**
|
||||
* @file
|
||||
* Defines JavaScript behaviors for the media type form.
|
||||
*/
|
||||
|
||||
(function($, Drupal) {
|
||||
/**
|
||||
* 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(context) {
|
||||
const $context = $(context);
|
||||
// Provide the vertical tab summaries.
|
||||
$context.find('#edit-workflow').drupalSetSummary(context => {
|
||||
const 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(context => {
|
||||
const 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);
|
||||
@@ -0,0 +1,36 @@
|
||||
/**
|
||||
* DO NOT EDIT THIS FILE.
|
||||
* See the following change record for more information,
|
||||
* https://www.drupal.org/node/2815083
|
||||
* @preserve
|
||||
**/
|
||||
|
||||
(function ($, Drupal) {
|
||||
Drupal.behaviors.mediaTypeFormSummaries = {
|
||||
attach: function attach(context) {
|
||||
var $context = $(context);
|
||||
|
||||
$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);
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Hooks related to Media and its plugins.
|
||||
*/
|
||||
|
||||
/**
|
||||
* @addtogroup hooks
|
||||
* @{
|
||||
*/
|
||||
|
||||
/**
|
||||
* Alters the information provided in \Drupal\media\Annotation\MediaSource.
|
||||
*
|
||||
* @param array $sources
|
||||
* The array of media source plugin definitions, keyed by plugin ID.
|
||||
*/
|
||||
function hook_media_source_info_alter(array &$sources) {
|
||||
$sources['youtube']['label'] = t('Youtube rocks!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Alters an oEmbed resource URL before it is fetched.
|
||||
*
|
||||
* @param array $parsed_url
|
||||
* A parsed URL, as returned by \Drupal\Component\Utility\UrlHelper::parse().
|
||||
* @param \Drupal\media\OEmbed\Provider $provider
|
||||
* The oEmbed provider for the resource.
|
||||
*
|
||||
* @see \Drupal\media\OEmbed\UrlResolverInterface::getResourceUrl()
|
||||
*/
|
||||
function hook_oembed_resource_url_alter(array &$parsed_url, \Drupal\media\OEmbed\Provider $provider) {
|
||||
// Always serve YouTube videos from youtube-nocookie.com.
|
||||
if ($provider->getName() === 'YouTube') {
|
||||
$parsed_url['path'] = str_replace('://youtube.com/', '://youtube-nocookie.com/', $parsed_url['path']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @} End of "addtogroup hooks".
|
||||
*/
|
||||
@@ -0,0 +1,11 @@
|
||||
name: Media
|
||||
description: 'Manages the creation, configuration, and display of media items.'
|
||||
type: module
|
||||
package: Core
|
||||
version: VERSION
|
||||
core: 8.x
|
||||
dependencies:
|
||||
- drupal:file
|
||||
- drupal:image
|
||||
- drupal:user
|
||||
configure: media.settings
|
||||
@@ -0,0 +1,259 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Install, uninstall and update hooks for Media module.
|
||||
*/
|
||||
|
||||
use Drupal\Core\Entity\Sql\SqlEntityStorageInterface;
|
||||
use Drupal\Core\File\Exception\FileException;
|
||||
use Drupal\Core\File\FileSystemInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\media\Entity\MediaType;
|
||||
use Drupal\media\MediaTypeInterface;
|
||||
use Drupal\media\Plugin\media\Source\OEmbedInterface;
|
||||
use Drupal\user\Entity\Role;
|
||||
use Drupal\user\RoleInterface;
|
||||
use Drupal\image\Plugin\Field\FieldType\ImageItem;
|
||||
use Drupal\Core\StringTranslation\TranslatableMarkup;
|
||||
|
||||
/**
|
||||
* Implements hook_install().
|
||||
*/
|
||||
function media_install() {
|
||||
$source = drupal_get_path('module', 'media') . '/images/icons';
|
||||
$destination = \Drupal::config('media.settings')->get('icon_base_uri');
|
||||
/** @var \Drupal\Core\File\FileSystemInterface $file_system */
|
||||
$file_system = \Drupal::service('file_system');
|
||||
$file_system->prepareDirectory($destination, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS);
|
||||
|
||||
$files = $file_system->scanDirectory($source, '/.*\.(svg|png|jpg|jpeg|gif)$/');
|
||||
foreach ($files as $file) {
|
||||
// When reinstalling the media module we don't want to copy the icons when
|
||||
// they already exist. The icons could be replaced (by a contrib module or
|
||||
// manually), so we don't want to replace the existing files. Removing the
|
||||
// files when we uninstall could also be a problem if the files are
|
||||
// referenced somewhere else. Since showing an error that it was not
|
||||
// possible to copy the files is also confusing, we silently do nothing.
|
||||
if (!file_exists($destination . DIRECTORY_SEPARATOR . $file->filename)) {
|
||||
try {
|
||||
$file_system->copy($file->uri, $destination, FileSystemInterface::EXISTS_ERROR);
|
||||
}
|
||||
catch (FileException $e) {
|
||||
// Ignore and continue.
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Grant the "view media" permission to all users by default.
|
||||
if (\Drupal::moduleHandler()->moduleExists('user')) {
|
||||
user_role_grant_permissions(RoleInterface::ANONYMOUS_ID, ['view media']);
|
||||
user_role_grant_permissions(RoleInterface::AUTHENTICATED_ID, ['view media']);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_requirements().
|
||||
*/
|
||||
function media_requirements($phase) {
|
||||
$requirements = [];
|
||||
if ($phase == 'install') {
|
||||
$destination = 'public://media-icons/generic';
|
||||
\Drupal::service('file_system')->prepareDirectory($destination, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::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 <a href=":handbook_url">online handbook</a>.', [':handbook_url' => 'https://www.drupal.org/server-permissions']);
|
||||
if (!empty($error)) {
|
||||
$description = $error . ' ' . $description;
|
||||
$requirements['media']['description'] = $description;
|
||||
$requirements['media']['severity'] = REQUIREMENT_ERROR;
|
||||
}
|
||||
}
|
||||
|
||||
// Prevent installation if the 1.x branch of the contrib module is enabled.
|
||||
if (\Drupal::moduleHandler()->moduleExists('media_entity')) {
|
||||
$info = \Drupal::service('extension.list.module')->getExtensionInfo('media_entity');
|
||||
if (version_compare($info['version'], '8.x-2') < 0) {
|
||||
$requirements['media_module_incompatibility'] = [
|
||||
'title' => t('Media'),
|
||||
'description' => t('The Media module is not compatible with contrib <a href=":url">Media Entity</a> 1.x branch. Please check the 2.x branch of that module for an upgrade path.', [
|
||||
':url' => 'https://drupal.org/project/media_entity',
|
||||
]),
|
||||
'severity' => REQUIREMENT_ERROR,
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
elseif ($phase === 'runtime') {
|
||||
// Check that oEmbed content is served in an iframe on a different domain,
|
||||
// and complain if it isn't.
|
||||
$domain = \Drupal::config('media.settings')->get('iframe_domain');
|
||||
|
||||
if (!\Drupal::service('media.oembed.iframe_url_helper')->isSecure($domain)) {
|
||||
// Find all media types which use a source plugin that implements
|
||||
// OEmbedInterface.
|
||||
$media_types = \Drupal::entityTypeManager()
|
||||
->getStorage('media_type')
|
||||
->loadMultiple();
|
||||
|
||||
$oembed_types = array_filter($media_types, function (MediaTypeInterface $media_type) {
|
||||
return $media_type->getSource() instanceof OEmbedInterface;
|
||||
});
|
||||
|
||||
if ($oembed_types) {
|
||||
// @todo Potentially allow site administrators to suppress this warning
|
||||
// permanently. See https://www.drupal.org/project/drupal/issues/2962753
|
||||
// for more information.
|
||||
$requirements['media_insecure_iframe'] = [
|
||||
'title' => t('Media'),
|
||||
'description' => t('It is potentially insecure to display oEmbed content in a frame that is served from the same domain as your main Drupal site, as this may allow execution of third-party code. <a href=":url">You can specify a different domain for serving oEmbed content here</a>.', [
|
||||
':url' => Url::fromRoute('media.settings')->setAbsolute()->toString(),
|
||||
]),
|
||||
'severity' => REQUIREMENT_WARNING,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// When a new media type with an image source is created we're configuring
|
||||
// the default entity view display using the 'large' image style.
|
||||
// Unfortunately, if a site builder has deleted the 'large' image style,
|
||||
// we need some other image style to use, but at this point, we can't
|
||||
// really know the site builder's intentions. So rather than do something
|
||||
// surprising, we're leaving the embedded media without an image style and
|
||||
// adding a warning that the site builder might want to add an image style.
|
||||
// @see Drupal\media\Plugin\media\Source\Image::prepareViewDisplay
|
||||
$module_handler = \Drupal::service('module_handler');
|
||||
foreach (MediaType::loadMultiple() as $type) {
|
||||
// Load the default display.
|
||||
$display = \Drupal::service('entity_display.repository')
|
||||
->getViewDisplay('media', $type->id());
|
||||
|
||||
$source_field_definition = $type->getSource()->getSourceFieldDefinition($type);
|
||||
if (!is_a($source_field_definition->getItemDefinition()->getClass(), ImageItem::class, TRUE)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$component = $display->getComponent($source_field_definition->getName());
|
||||
if (empty($component) || $component['type'] !== 'image' || !empty($component['settings']['image_style'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$action_item = '';
|
||||
if ($module_handler->moduleExists('field_ui') && \Drupal::currentUser()->hasPermission('administer media display')) {
|
||||
$url = Url::fromRoute('entity.entity_view_display.media.default', [
|
||||
'media_type' => $type->id(),
|
||||
])->toString();
|
||||
$action_item = new TranslatableMarkup('If you would like to change this, <a href=":display">add an image style to the %field_name field</a>.',
|
||||
[
|
||||
'%field_name' => $source_field_definition->label(),
|
||||
':display' => $url,
|
||||
]);
|
||||
}
|
||||
$requirements['media_default_image_style_' . $type->id()] = [
|
||||
'title' => t('Media'),
|
||||
'description' => new TranslatableMarkup('The default display for the %type media type is not currently using an image style on the %field_name field. Not using an image style can lead to much larger file downloads. @action_item',
|
||||
[
|
||||
'%field_name' => $source_field_definition->label(),
|
||||
'@action_item' => $action_item,
|
||||
'%type' => $type->label(),
|
||||
]
|
||||
),
|
||||
'severity' => REQUIREMENT_WARNING,
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return $requirements;
|
||||
}
|
||||
|
||||
/**
|
||||
* Introduce per-bundle permissions.
|
||||
*/
|
||||
function media_update_8500() {
|
||||
$media_types = \Drupal::entityQuery('media_type')->execute();
|
||||
|
||||
/** @var \Drupal\user\RoleInterface $role */
|
||||
foreach (Role::loadMultiple() as $role) {
|
||||
if ($role->hasPermission('update media')) {
|
||||
foreach ($media_types as $media_type) {
|
||||
$role->grantPermission("edit own $media_type media");
|
||||
}
|
||||
}
|
||||
|
||||
if ($role->hasPermission('update any media')) {
|
||||
foreach ($media_types as $media_type) {
|
||||
$role->grantPermission("edit any $media_type media");
|
||||
}
|
||||
}
|
||||
|
||||
if ($role->hasPermission('delete media')) {
|
||||
foreach ($media_types as $media_type) {
|
||||
$role->grantPermission("delete own $media_type media");
|
||||
}
|
||||
}
|
||||
|
||||
if ($role->hasPermission('delete any media')) {
|
||||
foreach ($media_types as $media_type) {
|
||||
$role->grantPermission("delete any $media_type media");
|
||||
}
|
||||
}
|
||||
|
||||
if ($role->hasPermission('create media')) {
|
||||
foreach ($media_types as $media_type) {
|
||||
$role->grantPermission("create $media_type media");
|
||||
}
|
||||
}
|
||||
|
||||
$role->save();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates media.settings to support OEmbed.
|
||||
*/
|
||||
function media_update_8600() {
|
||||
\Drupal::configFactory()->getEditable('media.settings')
|
||||
->set('iframe_domain', '')
|
||||
->set('oembed_providers_url', 'https://oembed.com/providers.json')
|
||||
->save(TRUE);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the 'owner' entity key and update the field.
|
||||
*/
|
||||
function media_update_8700() {
|
||||
$definition_update_manager = \Drupal::entityDefinitionUpdateManager();
|
||||
$entity_type = $definition_update_manager->getEntityType('media');
|
||||
$database = \Drupal::database();
|
||||
|
||||
if (\Drupal::entityTypeManager()->getStorage('media') instanceof SqlEntityStorageInterface) {
|
||||
if ($database->schema()->tableExists($entity_type->getDataTable())) {
|
||||
$database->update($entity_type->getDataTable())
|
||||
->fields(['uid' => 0])
|
||||
->isNull('uid')
|
||||
->execute();
|
||||
}
|
||||
|
||||
if ($database->schema()->tableExists($entity_type->getRevisionDataTable())) {
|
||||
$database->update($entity_type->getRevisionDataTable())
|
||||
->fields(['uid' => 0])
|
||||
->isNull('uid')
|
||||
->execute();
|
||||
}
|
||||
}
|
||||
|
||||
$keys = $entity_type->getKeys();
|
||||
$keys['owner'] = 'uid';
|
||||
$entity_type->set('entity_keys', $keys);
|
||||
$definition_update_manager->updateEntityType($entity_type);
|
||||
$definition_update_manager->updateFieldStorageDefinition($definition_update_manager->getFieldStorageDefinition('uid', 'media'));
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
form:
|
||||
version: VERSION
|
||||
js:
|
||||
js/form.js: {}
|
||||
dependencies:
|
||||
- core/drupal.form
|
||||
|
||||
type_form:
|
||||
version: VERSION
|
||||
js:
|
||||
js/type_form.js: {}
|
||||
dependencies:
|
||||
- core/drupal.form
|
||||
|
||||
oembed.formatter:
|
||||
version: VERSION
|
||||
css:
|
||||
component:
|
||||
css/oembed.formatter.css: {}
|
||||
|
||||
oembed.frame:
|
||||
version: VERSION
|
||||
css:
|
||||
component:
|
||||
css/oembed.frame.css: {}
|
||||
|
||||
filter.caption:
|
||||
version: VERSION
|
||||
css:
|
||||
component:
|
||||
css/filter.caption.css: {}
|
||||
dependencies:
|
||||
- filter/caption
|
||||
|
||||
media_embed_ckeditor_theme:
|
||||
version: VERSION
|
||||
js:
|
||||
js/media_embed_ckeditor.theme.js: {}
|
||||
dependencies:
|
||||
- core/drupal
|
||||
@@ -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:
|
||||
- entity.media.collection
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
entity.media_type.collection:
|
||||
title: 'Media types'
|
||||
parent: system.admin_structure
|
||||
description: 'Manage media types.'
|
||||
route_name: entity.media_type.collection
|
||||
|
||||
media.settings:
|
||||
title: 'Media settings'
|
||||
parent: system.admin_config_media
|
||||
description: 'Manage media settings.'
|
||||
route_name: media.settings
|
||||
@@ -0,0 +1,18 @@
|
||||
media.tasks:
|
||||
deriver: 'Drupal\media\Plugin\Derivative\DynamicLocalTasks'
|
||||
|
||||
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
|
||||
|
||||
entity.media.collection:
|
||||
title: Media
|
||||
route_name: entity.media.collection
|
||||
base_route: system.admin_content
|
||||
weight: 10
|
||||
@@ -0,0 +1,528 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Provides media items.
|
||||
*/
|
||||
|
||||
use Drupal\Component\Plugin\DerivativeInspectionInterface;
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Render\Element;
|
||||
use Drupal\Core\Render\Element\RenderElement;
|
||||
use Drupal\Core\Routing\RouteMatchInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\Core\Template\Attribute;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\field\FieldConfigInterface;
|
||||
use Drupal\media\Plugin\media\Source\OEmbedInterface;
|
||||
use Drupal\views\ViewExecutable;
|
||||
|
||||
/**
|
||||
* Implements hook_help().
|
||||
*/
|
||||
function media_help($route_name, RouteMatchInterface $route_match) {
|
||||
switch ($route_name) {
|
||||
case 'help.page.media':
|
||||
$output = '<h3>' . t('About') . '</h3>';
|
||||
$output .= '<p>' . 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 <a href=":media">online documentation for the Media module</a>.', [':media' => 'https://www.drupal.org/docs/8/core/modules/media']) . '</p>';
|
||||
$output .= '<h3>' . t('Uses') . '</h3>';
|
||||
$output .= '<dl>';
|
||||
$output .= '<dt>' . t('Creating media items') . '</dt>';
|
||||
$output .= '<dd>' . t('When a new media item is created, the Media module records basic information about it, including the author, date of creation, and the <a href=":media-type">media type</a>. It also manages the <em>publishing options</em>, 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()]) . '</dd>';
|
||||
$output .= '<dt>' . t('Listing media items') . '</dt>';
|
||||
$output .= '<dd>' . t('Media items are listed at the <a href=":media-collection">media administration page</a>.', [
|
||||
':media-collection' => Url::fromRoute('entity.media.collection')->toString(),
|
||||
]) . '</dd>';
|
||||
$output .= '<dt>' . t('Creating custom media types') . '</dt>';
|
||||
$output .= '<dd>' . t('The Media module gives users with the <em>Administer media types</em> permission the ability to <a href=":media-new">create new media types</a> 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 <a href=":field">Field module</a> 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(),
|
||||
]) . '</dd>';
|
||||
$output .= '<dt>' . t('Creating revisions') . '</dt>';
|
||||
$output .= '<dd>' . t('The Media module also enables you to create multiple versions of any media item, and revert to older versions using the <em>Revision information</em> settings.') . '</dd>';
|
||||
$output .= '<dt>' . t('User permissions') . '</dt>';
|
||||
$output .= '<dd>' . t('The Media module makes a number of permissions available, which can be set by role on the <a href=":permissions">permissions page</a>.', [
|
||||
':permissions' => Url::fromRoute('user.admin_permissions', [], ['fragment' => 'module-media'])->toString(),
|
||||
]) . '</dd>';
|
||||
$output .= '<dt>' . t('Adding media to other content') . '</dt>';
|
||||
$output .= '<dd>' . t('Users with permission to administer content types can add media support by adding a media reference field to the content type on the content type administration page. (The same is true of block types, taxonomy terms, user profiles, and other content that supports fields.) A media reference field can refer to any configured media type. It is possible to allow multiple media types in the same field.') . '</dd>';
|
||||
$output .= '</dl>';
|
||||
$output .= '<h3>' . t('Differences between Media, File, and Image reference fields') . '</h3>';
|
||||
$output .= '<p>' . t('<em>Media</em> reference fields offer several advantages over basic <em>File</em> and <em>Image</em> references:') . '</p>';
|
||||
$output .= '<ul>';
|
||||
$output .= '<li>' . t('Media reference fields can reference multiple media types in the same field.') . '</li>';
|
||||
$output .= '<li>' . t('Fields can also be added to media types themselves, which means that custom metadata like descriptions and taxonomy tags can be added for the referenced media. (Basic file and image fields do not support this.)') . '</li>';
|
||||
$output .= '<li>' . t('Media types for audio and video files are provided by default, so there is no need for additional configuration to upload these media.') . '</li>';
|
||||
$output .= '<li>' . t('Contributed or custom projects can provide additional media sources (such as third-party websites, Twitter, etc.).') . '</li>';
|
||||
$output .= '<li>' . t('Existing media items can be reused on any other content items with a media reference field.') . '</li>';
|
||||
$output .= '</ul>';
|
||||
$output .= '<p>' . t('Use <em>Media</em> reference fields for most files, images, audio, videos, and remote media. Use <em>File</em> or <em>Image</em> reference fields when creating your own media types, or for legacy files and images created before enabling the Media module.') . '</p>';
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_theme().
|
||||
*/
|
||||
function media_theme() {
|
||||
return [
|
||||
'media' => [
|
||||
'render element' => 'elements',
|
||||
],
|
||||
'media_reference_help' => [
|
||||
'render element' => 'element',
|
||||
'base hook' => 'field_multiple_value_form',
|
||||
],
|
||||
'media_oembed_iframe' => [
|
||||
'variables' => [
|
||||
'media' => NULL,
|
||||
'placeholder_token' => '',
|
||||
],
|
||||
],
|
||||
'media_embed_error' => [
|
||||
'variables' => [
|
||||
'message' => NULL,
|
||||
'attributes' => [],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 = [];
|
||||
/** @var \Drupal\media\MediaInterface $media */
|
||||
$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;
|
||||
|
||||
// Add suggestions based on the source plugin ID.
|
||||
$source = $media->getSource();
|
||||
if ($source instanceof DerivativeInspectionInterface) {
|
||||
$source_id = $source->getBaseId();
|
||||
$derivative_id = $source->getDerivativeId();
|
||||
if ($derivative_id) {
|
||||
$source_id .= '__derivative_' . $derivative_id;
|
||||
}
|
||||
}
|
||||
else {
|
||||
$source_id = $source->getPluginId();
|
||||
}
|
||||
$suggestions[] = "media__source_$source_id";
|
||||
|
||||
// If the source plugin uses oEmbed, add a suggestion based on the provider
|
||||
// name, if available.
|
||||
if ($source instanceof OEmbedInterface) {
|
||||
$provider_id = $source->getMetadata($media, 'provider_name');
|
||||
if ($provider_id) {
|
||||
$provider_id = \Drupal::transliteration()->transliterate($provider_id);
|
||||
$provider_id = preg_replace('/[^a-z0-9_]+/', '_', mb_strtolower($provider_id));
|
||||
$suggestions[] = end($suggestions) . "__provider_$provider_id";
|
||||
}
|
||||
}
|
||||
|
||||
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];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_field_ui_preconfigured_options_alter().
|
||||
*/
|
||||
function media_field_ui_preconfigured_options_alter(array &$options, $field_type) {
|
||||
// If the field is not an "entity_reference"-based field, bail out.
|
||||
/** @var \Drupal\Core\Field\FieldTypePluginManager $field_type_manager */
|
||||
$field_type_manager = \Drupal::service('plugin.manager.field.field_type');
|
||||
$class = $field_type_manager->getPluginClass($field_type);
|
||||
if (!is_a($class, 'Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem', TRUE)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Set the default formatter for media in entity reference fields to be the
|
||||
// "Rendered entity" formatter.
|
||||
if (!empty($options['media'])) {
|
||||
$options['media']['entity_view_display']['type'] = 'entity_reference_entity_view';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_form_FORM_ID_alter().
|
||||
*/
|
||||
function media_form_field_ui_field_storage_add_form_alter(&$form, FormStateInterface $form_state, $form_id) {
|
||||
// Provide some help text to aid users decide whether they need a Media,
|
||||
// File, or Image reference field.
|
||||
$description_text = t('Use <em>Media</em> reference fields for most files, images, audio, videos, and remote media. Use <em>File</em> or <em>Image</em> reference fields when creating your own media types, or for legacy files and images created before enabling the Media module.');
|
||||
if (\Drupal::moduleHandler()->moduleExists('help')) {
|
||||
$description_text .= ' ' . t('For more information, see the <a href="@help_url">Media help page</a>.', [
|
||||
'@help_url' => Url::fromRoute('help.page', ['name' => 'media'])->toString(),
|
||||
]);
|
||||
}
|
||||
$form['add']['description_wrapper'] = [
|
||||
'#type' => 'container',
|
||||
];
|
||||
$field_types = [
|
||||
'file',
|
||||
'image',
|
||||
'field_ui:entity_reference:media',
|
||||
];
|
||||
foreach ($field_types as $field_name) {
|
||||
$form['add']['description_wrapper']["description_{$field_name}"] = [
|
||||
'#type' => 'item',
|
||||
'#markup' => $description_text,
|
||||
'#states' => [
|
||||
'visible' => [
|
||||
':input[name="new_storage_type"]' => ['value' => $field_name],
|
||||
],
|
||||
],
|
||||
];
|
||||
}
|
||||
$form['add']['new_storage_type']['#weight'] = 0;
|
||||
$form['add']['description_wrapper']['#weight'] = 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_field_widget_multivalue_form_alter().
|
||||
*/
|
||||
function media_field_widget_multivalue_form_alter(array &$elements, FormStateInterface $form_state, array $context) {
|
||||
// Do not alter the default settings form.
|
||||
if ($context['default']) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Only act on entity reference fields that reference media.
|
||||
$field_type = $context['items']->getFieldDefinition()->getType();
|
||||
$target_type = $context['items']->getFieldDefinition()->getFieldStorageDefinition()->getSetting('target_type');
|
||||
if ($field_type !== 'entity_reference' || $target_type !== 'media') {
|
||||
return;
|
||||
}
|
||||
|
||||
// Autocomplete widgets need different help text than options widgets.
|
||||
$widget_plugin_id = $context['widget']->getPluginId();
|
||||
if (in_array($widget_plugin_id, ['entity_reference_autocomplete', 'entity_reference_autocomplete_tags'])) {
|
||||
$is_autocomplete = TRUE;
|
||||
}
|
||||
else {
|
||||
// @todo We can't yet properly alter non-autocomplete fields. Resolve this
|
||||
// in https://www.drupal.org/node/2943020 and remove this condition.
|
||||
return;
|
||||
}
|
||||
$elements['#media_help'] = [];
|
||||
|
||||
// Retrieve the media bundle list and add information for the user based on
|
||||
// which bundles are available to be created or referenced.
|
||||
$settings = $context['items']->getFieldDefinition()->getSetting('handler_settings');
|
||||
$allowed_bundles = !empty($settings['target_bundles']) ? $settings['target_bundles'] : [];
|
||||
$add_url = _media_get_add_url($allowed_bundles);
|
||||
if ($add_url) {
|
||||
$elements['#media_help']['#media_add_help'] = t('Create your media on the <a href=":add_page" target="_blank">media add page</a> (opens a new window), then add it by name to the field below.', [':add_page' => $add_url]);
|
||||
}
|
||||
|
||||
$elements['#theme'] = 'media_reference_help';
|
||||
// @todo template_preprocess_field_multiple_value_form() assumes this key
|
||||
// exists, but it does not exist in the case of a single widget that
|
||||
// accepts multiple values. This is for some reason necessary to use
|
||||
// our template for the entity_autocomplete_tags widget.
|
||||
// Research and resolve this in https://www.drupal.org/node/2943020.
|
||||
if (empty($elements['#cardinality_multiple'])) {
|
||||
$elements['#cardinality_multiple'] = NULL;
|
||||
}
|
||||
|
||||
// Use the title set on the element if it exists, otherwise fall back to the
|
||||
// field label.
|
||||
$elements['#media_help']['#original_label'] = isset($elements['#title']) ? $elements['#title'] : $context['items']->getFieldDefinition()->getLabel();
|
||||
|
||||
// Customize the label for the field widget.
|
||||
// @todo Research a better approach https://www.drupal.org/node/2943024.
|
||||
$use_existing_label = t('Use existing media');
|
||||
if (!empty($elements[0]['target_id']['#title'])) {
|
||||
$elements[0]['target_id']['#title'] = $use_existing_label;
|
||||
}
|
||||
if (!empty($elements['#title'])) {
|
||||
$elements['#title'] = $use_existing_label;
|
||||
}
|
||||
if (!empty($elements['target_id']['#title'])) {
|
||||
$elements['target_id']['#title'] = $use_existing_label;
|
||||
}
|
||||
|
||||
// This help text is only relevant for autocomplete widgets. When the user
|
||||
// is presented with options, they don't need to type anything or know what
|
||||
// types of media are allowed.
|
||||
if ($is_autocomplete) {
|
||||
$elements['#media_help']['#media_list_help'] = t('Type part of the media name.');
|
||||
|
||||
$overview_url = Url::fromRoute('entity.media.collection');
|
||||
if ($overview_url->access()) {
|
||||
$elements['#media_help']['#media_list_link'] = t('See the <a href=":list_url" target="_blank">media list</a> (opens a new window) to help locate media.', [':list_url' => $overview_url->toString()]);
|
||||
}
|
||||
$all_bundles = \Drupal::service('entity_type.bundle.info')->getBundleInfo('media');
|
||||
$bundle_labels = array_map(function ($bundle) use ($all_bundles) {
|
||||
return $all_bundles[$bundle]['label'];
|
||||
}, $allowed_bundles);
|
||||
$elements['#media_help']['#allowed_types_help'] = t('Allowed media types: %types', ['%types' => implode(", ", $bundle_labels)]);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_preprocess_HOOK() for media reference widgets.
|
||||
*/
|
||||
function media_preprocess_media_reference_help(&$variables) {
|
||||
// Most of these attribute checks are copied from
|
||||
// template_preprocess_fieldset(). Our template extends
|
||||
// field-multiple-value-form.html.twig to provide our help text, but also
|
||||
// groups the information within a semantic fieldset with a legend. So, we
|
||||
// incorporate parity for both.
|
||||
$element = $variables['element'];
|
||||
Element::setAttributes($element, ['id']);
|
||||
RenderElement::setAttributes($element);
|
||||
$variables['attributes'] = isset($element['#attributes']) ? $element['#attributes'] : [];
|
||||
$variables['legend_attributes'] = new Attribute();
|
||||
$variables['header_attributes'] = new Attribute();
|
||||
$variables['description']['attributes'] = new Attribute();
|
||||
$variables['legend_span_attributes'] = new Attribute();
|
||||
|
||||
if (!empty($element['#media_help'])) {
|
||||
foreach ($element['#media_help'] as $key => $text) {
|
||||
$variables[substr($key, 1)] = $text;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the appropriate URL to add media for the current user.
|
||||
*
|
||||
* @todo Remove in https://www.drupal.org/project/drupal/issues/2938116
|
||||
*
|
||||
* @param string[] $allowed_bundles
|
||||
* An array of bundles that should be checked for create access.
|
||||
*
|
||||
* @return bool|\Drupal\Core\Url
|
||||
* The URL to add media, or FALSE if the user cannot create any media.
|
||||
*
|
||||
* @internal
|
||||
* This function is internal and may be removed in a minor release.
|
||||
*/
|
||||
function _media_get_add_url($allowed_bundles) {
|
||||
$access_handler = \Drupal::entityTypeManager()->getAccessControlHandler('media');
|
||||
$create_bundles = array_filter($allowed_bundles, [$access_handler, 'createAccess']);
|
||||
|
||||
// Add a section about how to create media if the user has access to do so.
|
||||
if (count($create_bundles) === 1) {
|
||||
return Url::fromRoute('entity.media.add_form', ['media_type' => reset($create_bundles)])->toString();
|
||||
}
|
||||
elseif (count($create_bundles) > 1) {
|
||||
return Url::fromRoute('entity.media.add_page')->toString();
|
||||
}
|
||||
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_entity_type_alter().
|
||||
*/
|
||||
function media_entity_type_alter(array &$entity_types) {
|
||||
if (\Drupal::config('media.settings')->get('standalone_url')) {
|
||||
/** @var \Drupal\Core\Entity\ContentEntityTypeInterface $entity_type */
|
||||
$entity_type = $entity_types['media'];
|
||||
$entity_type->setLinkTemplate('canonical', '/media/{media}');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_form_FORM_ID_alter().
|
||||
*/
|
||||
function media_form_filter_format_edit_form_alter(array &$form, FormStateInterface $form_state, $form_id) {
|
||||
// Add an additional validate callback so we can ensure the order of filters
|
||||
// is correct.
|
||||
$form['#validate'][] = 'media_filter_format_edit_form_validate';
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_form_FORM_ID_alter().
|
||||
*/
|
||||
function media_form_filter_format_add_form_alter(array &$form, FormStateInterface $form_state, $form_id) {
|
||||
// Add an additional validate callback so we can ensure the order of filters
|
||||
// is correct.
|
||||
$form['#validate'][] = 'media_filter_format_edit_form_validate';
|
||||
}
|
||||
|
||||
/**
|
||||
* Validate callback to ensure filter order and allowed_html are compatible.
|
||||
*/
|
||||
function media_filter_format_edit_form_validate($form, FormStateInterface $form_state) {
|
||||
if ($form_state->getTriggeringElement()['#name'] !== 'op') {
|
||||
return;
|
||||
}
|
||||
|
||||
$allowed_html_path = [
|
||||
'filters',
|
||||
'filter_html',
|
||||
'settings',
|
||||
'allowed_html',
|
||||
];
|
||||
|
||||
$filter_html_settings_path = [
|
||||
'filters',
|
||||
'filter_html',
|
||||
'settings',
|
||||
];
|
||||
|
||||
$filter_html_enabled = $form_state->getValue([
|
||||
'filters',
|
||||
'filter_html',
|
||||
'status',
|
||||
]);
|
||||
|
||||
$media_embed_enabled = $form_state->getValue([
|
||||
'filters',
|
||||
'media_embed',
|
||||
'status',
|
||||
]);
|
||||
|
||||
if (!$media_embed_enabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
$get_filter_label = function ($filter_plugin_id) use ($form) {
|
||||
return (string) $form['filters']['order'][$filter_plugin_id]['filter']['#markup'];
|
||||
};
|
||||
|
||||
if ($filter_html_enabled && $form_state->getValue($allowed_html_path)) {
|
||||
/** @var \Drupal\filter\Entity\FilterFormat $filter_format */
|
||||
$filter_format = $form_state->getFormObject()->getEntity();
|
||||
|
||||
$filter_html = clone $filter_format->filters()->get('filter_html');
|
||||
$filter_html->setConfiguration(['settings' => $form_state->getValue($filter_html_settings_path)]);
|
||||
$restrictions = $filter_html->getHTMLRestrictions();
|
||||
$allowed = $restrictions['allowed'];
|
||||
|
||||
// Require `<drupal-media>` HTML tag if filter_html is enabled.
|
||||
if (!isset($allowed['drupal-media'])) {
|
||||
$form_state->setError($form['filters']['settings']['filter_html']['allowed_html'], t('The %media-embed-filter-label filter requires <code><drupal-media></code> among the allowed HTML tags.', [
|
||||
'%media-embed-filter-label' => $get_filter_label('media_embed'),
|
||||
]));
|
||||
}
|
||||
else {
|
||||
$required_attributes = [
|
||||
'data-entity-type',
|
||||
'data-entity-uuid',
|
||||
];
|
||||
|
||||
// If there are no attributes, the allowed item is set to FALSE,
|
||||
// otherwise, it is set to an array.
|
||||
if ($allowed['drupal-media'] === FALSE) {
|
||||
$missing_attributes = $required_attributes;
|
||||
}
|
||||
else {
|
||||
$missing_attributes = array_diff($required_attributes, array_keys($allowed['drupal-media']));
|
||||
}
|
||||
|
||||
if ($missing_attributes) {
|
||||
$form_state->setError($form['filters']['settings']['filter_html']['allowed_html'], t('The <code><drupal-media></code> tag in the allowed HTML tags is missing the following attributes: <code>%list</code>.', [
|
||||
'%list' => implode(', ', $missing_attributes),
|
||||
]));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$filters = $form_state->getValue('filters');
|
||||
|
||||
// The "media_embed" filter must run after "filter_align", "filter_caption",
|
||||
// and "filter_html_image_secure".
|
||||
$precedents = [
|
||||
'filter_align',
|
||||
'filter_caption',
|
||||
'filter_html_image_secure',
|
||||
];
|
||||
|
||||
$error_filters = [];
|
||||
foreach ($precedents as $filter_name) {
|
||||
// A filter that should run before media embed filter.
|
||||
$precedent = $filters[$filter_name];
|
||||
|
||||
if (empty($precedent['status']) || !isset($precedent['weight'])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($precedent['weight'] >= $filters['media_embed']['weight']) {
|
||||
$error_filters[$filter_name] = $get_filter_label($filter_name);
|
||||
}
|
||||
}
|
||||
|
||||
if (!empty($error_filters)) {
|
||||
$error_message = \Drupal::translation()->formatPlural(
|
||||
count($error_filters),
|
||||
'The %media-embed-filter-label filter needs to be placed after the %filter filter.',
|
||||
'The %media-embed-filter-label filter needs to be placed after the following filters: %filters.',
|
||||
[
|
||||
'%media-embed-filter-label' => $get_filter_label('media_embed'),
|
||||
'%filter' => reset($error_filters),
|
||||
'%filters' => implode(', ', $error_filters),
|
||||
]
|
||||
);
|
||||
|
||||
$form_state->setErrorByName('filters', $error_message);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_field_widget_form_alter().
|
||||
*/
|
||||
function media_field_widget_form_alter(&$element, FormStateInterface $form_state, $context) {
|
||||
// Add an attribute so that "drupalmedia" CKEditor plugin can pass the host
|
||||
// entity's language to EditorMediaDialog, allowing it to present entities
|
||||
// in the same language.
|
||||
if (!empty($element['#type']) && $element['#type'] == 'text_format') {
|
||||
$element['#attributes']['data-media-embed-host-entity-langcode'] = $context['items']->getLangcode();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_views_query_substitutions().
|
||||
*/
|
||||
function media_views_query_substitutions(ViewExecutable $view) {
|
||||
$account = \Drupal::currentUser();
|
||||
return [
|
||||
'***VIEW_OWN_UNPUBLISHED_MEDIA***' => (int) $account->hasPermission('view own unpublished media'),
|
||||
'***ADMINISTER_MEDIA***' => (int) $account->hasPermission('administer media'),
|
||||
];
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
administer media:
|
||||
title: 'Administer media'
|
||||
restrict access: TRUE
|
||||
|
||||
administer media types:
|
||||
title: 'Administer media types'
|
||||
restrict access: TRUE
|
||||
|
||||
view media:
|
||||
title: 'View media'
|
||||
|
||||
# @todo: Deprecate some permissions in https://www.drupal.org/project/drupal/issues/2925459
|
||||
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'
|
||||
|
||||
view all media revisions:
|
||||
title: 'View all media revisions'
|
||||
description: 'To view a revision, you also need permission to view the media item.'
|
||||
|
||||
access media overview:
|
||||
title: 'Access media overview'
|
||||
description: 'Users with this permission can access the media overview page.'
|
||||
|
||||
permission_callbacks:
|
||||
- \Drupal\media\MediaPermissions::mediaTypePermissions
|
||||
|
||||
view own unpublished media:
|
||||
title: 'View own unpublished media'
|
||||
@@ -0,0 +1,99 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Post update functions for Media.
|
||||
*/
|
||||
|
||||
use Drupal\user\RoleInterface;
|
||||
use Drupal\views\Views;
|
||||
|
||||
/**
|
||||
* Clear caches due to changes in local tasks and action links.
|
||||
*/
|
||||
function media_post_update_collection_route() {
|
||||
// Empty post-update hook.
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear caches due to the addition of a Media-specific entity storage handler.
|
||||
*/
|
||||
function media_post_update_storage_handler() {
|
||||
// Empty post-update hook.
|
||||
}
|
||||
|
||||
/**
|
||||
* Keep media items viewable at /media/{id}.
|
||||
*/
|
||||
function media_post_update_enable_standalone_url() {
|
||||
$config = \Drupal::configFactory()->getEditable('media.settings');
|
||||
if ($config->get('standalone_url') === NULL) {
|
||||
$config->set('standalone_url', TRUE)->save(TRUE);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a status extra filter to the media view default display.
|
||||
*/
|
||||
function media_post_update_add_status_extra_filter() {
|
||||
$view = Views::getView('media');
|
||||
|
||||
if (!$view) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Fetch the filters from the default display and add the new 'status_extra'
|
||||
// filter if it does not yet exist.
|
||||
$default_display = $view->getDisplay();
|
||||
$filters = $default_display->getOption('filters');
|
||||
|
||||
if (!isset($filters['status_extra'])) {
|
||||
$filters['status_extra'] = [
|
||||
'group_info' => [
|
||||
'widget' => 'select',
|
||||
'group_items' => [],
|
||||
'multiple' => FALSE,
|
||||
'description' => '',
|
||||
'default_group_multiple' => [],
|
||||
'default_group' => 'All',
|
||||
'label' => '',
|
||||
'identifier' => '',
|
||||
'optional' => TRUE,
|
||||
'remember' => FALSE,
|
||||
],
|
||||
'group' => 1,
|
||||
'relationship' => 'none',
|
||||
'exposed' => FALSE,
|
||||
'expose' => [
|
||||
'use_operator' => FALSE,
|
||||
'remember' => FALSE,
|
||||
'operator_id' => '',
|
||||
'multiple' => FALSE,
|
||||
'description' => '',
|
||||
'required' => FALSE,
|
||||
'label' => '',
|
||||
'operator_limit_selection' => FALSE,
|
||||
'operator' => '',
|
||||
'identifier' => '',
|
||||
'operator_list' => [],
|
||||
'remember_roles' => [RoleInterface::AUTHENTICATED_ID => RoleInterface::AUTHENTICATED_ID],
|
||||
],
|
||||
'entity_type' => 'media',
|
||||
'value' => '',
|
||||
'field' => 'status_extra',
|
||||
'is_grouped' => FALSE,
|
||||
'admin_label' => '',
|
||||
'operator' => '=',
|
||||
'table' => 'media_field_data',
|
||||
'plugin_id' => 'media_status',
|
||||
'id' => 'status_extra',
|
||||
'group_type' => 'group',
|
||||
];
|
||||
$default_display->setOption('filters', $filters);
|
||||
$view->save();
|
||||
|
||||
return t("The 'Published status or admin user' filter was added to the %label view.", [
|
||||
'%label' => $view->storage->label(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
# @deprecated in Drupal 8.6.x, to be removed before Drupal 9.0.0.
|
||||
# This route is not used in Drupal core. As an internal API, it may also be
|
||||
# removed in a minor release. If you are using it, copy the class
|
||||
# and the related "entity.media.multiple_delete_confirm" route to your
|
||||
# module.
|
||||
entity.media.multiple_delete_confirm:
|
||||
path: '/admin/content/media/delete'
|
||||
defaults:
|
||||
_form: '\Drupal\media\Form\MediaDeleteMultipleConfirmForm'
|
||||
requirements:
|
||||
_permission: 'administer media+delete any media'
|
||||
|
||||
entity.media.revision:
|
||||
path: '/media/{media}/revisions/{media_revision}/view'
|
||||
defaults:
|
||||
_controller: '\Drupal\Core\Entity\Controller\EntityViewController::viewRevision'
|
||||
_title_callback: '\Drupal\Core\Entity\Controller\EntityController::title'
|
||||
options:
|
||||
parameters:
|
||||
media:
|
||||
type: entity:media
|
||||
media_revision:
|
||||
type: entity_revision:media
|
||||
requirements:
|
||||
_access_media_revision: 'view'
|
||||
media: \d+
|
||||
|
||||
media.oembed_iframe:
|
||||
path: '/media/oembed'
|
||||
defaults:
|
||||
_controller: '\Drupal\media\Controller\OEmbedIframeController::render'
|
||||
requirements:
|
||||
_permission: 'view media'
|
||||
|
||||
media.settings:
|
||||
path: '/admin/config/media/media-settings'
|
||||
defaults:
|
||||
_form: '\Drupal\media\Form\MediaSettingsForm'
|
||||
_title: 'Media settings'
|
||||
requirements:
|
||||
_permission: 'administer media'
|
||||
|
||||
media.filter.preview:
|
||||
path: '/media/{filter_format}/preview'
|
||||
defaults:
|
||||
_controller: '\Drupal\media\Controller\MediaFilterController::preview'
|
||||
methods: [GET]
|
||||
requirements:
|
||||
_entity_access: 'filter_format.use'
|
||||
_custom_access: '\Drupal\media\Controller\MediaFilterController::formatUsesMediaEmbedFilter'
|
||||
|
||||
editor.media_dialog:
|
||||
path: '/editor/dialog/media/{editor}'
|
||||
defaults:
|
||||
_form: '\Drupal\media\Form\EditorMediaDialog'
|
||||
_title: 'Edit media'
|
||||
methods: [POST]
|
||||
requirements:
|
||||
_entity_access: 'editor.use'
|
||||
@@ -0,0 +1,26 @@
|
||||
services:
|
||||
plugin.manager.media.source:
|
||||
class: Drupal\media\MediaSourceManager
|
||||
parent: default_plugin_manager
|
||||
access_check.media.revision:
|
||||
class: Drupal\media\Access\MediaRevisionAccessCheck
|
||||
arguments: ['@entity_type.manager']
|
||||
tags:
|
||||
- { name: access_check, applies_to: _access_media_revision }
|
||||
media.oembed.url_resolver:
|
||||
class: Drupal\media\OEmbed\UrlResolver
|
||||
arguments: ['@media.oembed.provider_repository', '@media.oembed.resource_fetcher', '@http_client', '@module_handler', '@cache.default']
|
||||
media.oembed.provider_repository:
|
||||
class: Drupal\media\OEmbed\ProviderRepository
|
||||
arguments: ['@http_client', '@config.factory', '@datetime.time', '@cache.default']
|
||||
media.oembed.resource_fetcher:
|
||||
class: Drupal\media\OEmbed\ResourceFetcher
|
||||
arguments: ['@http_client', '@media.oembed.provider_repository', '@cache.default']
|
||||
media.oembed.iframe_url_helper:
|
||||
class: Drupal\media\IFrameUrlHelper
|
||||
arguments: ['@router.request_context', '@private_key']
|
||||
media.config_subscriber:
|
||||
class: Drupal\media\EventSubscriber\MediaConfigSubscriber
|
||||
arguments: ['@router.builder', '@cache_tags.invalidator', '@entity_type.manager']
|
||||
tags:
|
||||
- { name: event_subscriber }
|
||||
@@ -0,0 +1,151 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media\Access;
|
||||
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Routing\Access\AccessInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\media\MediaInterface;
|
||||
use Symfony\Component\Routing\Route;
|
||||
|
||||
/**
|
||||
* Provides an access checker for media item revisions.
|
||||
*
|
||||
* @ingroup media_access
|
||||
*/
|
||||
class MediaRevisionAccessCheck implements AccessInterface {
|
||||
|
||||
/**
|
||||
* The media storage.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\ContentEntityStorageInterface
|
||||
*/
|
||||
protected $mediaStorage;
|
||||
|
||||
/**
|
||||
* The media access control handler.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityAccessControlHandlerInterface
|
||||
*/
|
||||
protected $mediaAccess;
|
||||
|
||||
/**
|
||||
* A static cache of access checks.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $access = [];
|
||||
|
||||
/**
|
||||
* Constructs a new MediaRevisionAccessCheck.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
*/
|
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
|
||||
$this->mediaStorage = $entity_type_manager->getStorage('media');
|
||||
$this->mediaAccess = $entity_type_manager->getAccessControlHandler('media');
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks routing access for the media item revision.
|
||||
*
|
||||
* @param \Symfony\Component\Routing\Route $route
|
||||
* The route to check against.
|
||||
* @param \Drupal\Core\Session\AccountInterface $account
|
||||
* The currently logged in account.
|
||||
* @param int $media_revision
|
||||
* (optional) The media item revision ID. If not specified, but $media is,
|
||||
* access is checked for that object's revision.
|
||||
* @param \Drupal\media\MediaInterface $media
|
||||
* (optional) A media item. Used for checking access to a media items
|
||||
* default revision when $media_revision is unspecified. Ignored when
|
||||
* $media_revision is specified. If neither $media_revision nor $media are
|
||||
* specified, then access is denied.
|
||||
*
|
||||
* @return \Drupal\Core\Access\AccessResultInterface
|
||||
* The access result.
|
||||
*/
|
||||
public function access(Route $route, AccountInterface $account, $media_revision = NULL, MediaInterface $media = NULL) {
|
||||
if ($media_revision) {
|
||||
$media = $this->mediaStorage->loadRevision($media_revision);
|
||||
}
|
||||
$operation = $route->getRequirement('_access_media_revision');
|
||||
return AccessResult::allowedIf($media && $this->checkAccess($media, $account, $operation))->cachePerPermissions()->addCacheableDependency($media);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks media item revision access.
|
||||
*
|
||||
* @param \Drupal\media\MediaInterface $media
|
||||
* The media item to check.
|
||||
* @param \Drupal\Core\Session\AccountInterface $account
|
||||
* A user object representing the user for whom the operation is to be
|
||||
* performed.
|
||||
* @param string $op
|
||||
* (optional) The specific operation being checked. Defaults to 'view'.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the operation may be performed, FALSE otherwise.
|
||||
*/
|
||||
public function checkAccess(MediaInterface $media, AccountInterface $account, $op = 'view') {
|
||||
if (!$media || $op !== 'view') {
|
||||
// If there was no media to check against, or the $op was not one of the
|
||||
// supported ones, we return access denied.
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// Statically cache access by revision ID, language code, user account ID,
|
||||
// and operation.
|
||||
$langcode = $media->language()->getId();
|
||||
$cid = $media->getRevisionId() . ':' . $langcode . ':' . $account->id() . ':' . $op;
|
||||
|
||||
if (!isset($this->access[$cid])) {
|
||||
// Perform basic permission checks first.
|
||||
if (!$account->hasPermission('view all media revisions') && !$account->hasPermission('administer media')) {
|
||||
$this->access[$cid] = FALSE;
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// There should be at least two revisions. If the revision ID of the
|
||||
// given media item and the revision ID of the default revision differ,
|
||||
// then we already have two different revisions so there is no need for a
|
||||
// separate database check.
|
||||
if ($media->isDefaultRevision() && ($this->countDefaultLanguageRevisions($media) == 1)) {
|
||||
$this->access[$cid] = FALSE;
|
||||
}
|
||||
elseif ($account->hasPermission('administer media')) {
|
||||
$this->access[$cid] = TRUE;
|
||||
}
|
||||
else {
|
||||
// First check the access to the default revision and finally, if the
|
||||
// media passed in is not the default revision then access to that, too.
|
||||
$this->access[$cid] = $this->mediaAccess->access($this->mediaStorage->load($media->id()), $op, $account) && ($media->isDefaultRevision() || $this->mediaAccess->access($media, $op, $account));
|
||||
}
|
||||
}
|
||||
|
||||
return $this->access[$cid];
|
||||
}
|
||||
|
||||
/**
|
||||
* Counts the number of revisions in the default language.
|
||||
*
|
||||
* @param \Drupal\media\MediaInterface $media
|
||||
* The media item for which to count the revisions.
|
||||
*
|
||||
* @return int
|
||||
* The number of revisions in the default language.
|
||||
*/
|
||||
protected function countDefaultLanguageRevisions(MediaInterface $media) {
|
||||
$entity_type = $media->getEntityType();
|
||||
$count = $this->mediaStorage->getQuery()
|
||||
->allRevisions()
|
||||
->condition($entity_type->getKey('id'), $media->id())
|
||||
->condition($entity_type->getKey('default_langcode'), 1)
|
||||
->count()
|
||||
->execute();
|
||||
return $count;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media\Annotation;
|
||||
|
||||
use Drupal\Component\Annotation\Plugin;
|
||||
|
||||
/**
|
||||
* Defines a media source plugin annotation object.
|
||||
*
|
||||
* Media sources are responsible for implementing all the logic for dealing
|
||||
* with a particular type of media. They provide various universal and
|
||||
* type-specific metadata about media of the type they handle.
|
||||
*
|
||||
* Plugin namespace: Plugin\media\Source
|
||||
*
|
||||
* For a working example, see \Drupal\media\Plugin\media\Source\File.
|
||||
*
|
||||
* @see \Drupal\media\MediaSourceInterface
|
||||
* @see \Drupal\media\MediaSourceBase
|
||||
* @see \Drupal\media\MediaSourceManager
|
||||
* @see hook_media_source_info_alter()
|
||||
* @see plugin_api
|
||||
*
|
||||
* @Annotation
|
||||
*/
|
||||
class MediaSource extends Plugin {
|
||||
|
||||
/**
|
||||
* The plugin ID.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $id;
|
||||
|
||||
/**
|
||||
* The human-readable name of the media source.
|
||||
*
|
||||
* @var \Drupal\Core\Annotation\Translation
|
||||
*
|
||||
* @ingroup plugin_translatable
|
||||
*/
|
||||
public $label;
|
||||
|
||||
/**
|
||||
* A brief description of the media source.
|
||||
*
|
||||
* @var \Drupal\Core\Annotation\Translation
|
||||
*
|
||||
* @ingroup plugin_translatable
|
||||
*/
|
||||
public $description = '';
|
||||
|
||||
/**
|
||||
* The field types that can be used as a source field for this media source.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
public $allowed_field_types = [];
|
||||
|
||||
/**
|
||||
* The classes used to define media source-specific forms.
|
||||
*
|
||||
* An array of form class names, keyed by ID. The ID represents the operation
|
||||
* the form is used for.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
public $forms = [];
|
||||
|
||||
/**
|
||||
* A filename for the default thumbnail.
|
||||
*
|
||||
* The thumbnails are placed in the directory defined by the config setting
|
||||
* 'media.settings.icon_base_uri'. When using custom icons, make sure the
|
||||
* module provides a hook_install() implementation to copy the custom icons
|
||||
* to this directory. The media_install() function provides a clear example
|
||||
* of how to do this.
|
||||
*
|
||||
* @var string
|
||||
*
|
||||
* @see media_install()
|
||||
*/
|
||||
public $default_thumbnail_filename = 'generic.png';
|
||||
|
||||
/**
|
||||
* The metadata attribute name to provide the thumbnail URI.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $thumbnail_uri_metadata_attribute = 'thumbnail_uri';
|
||||
|
||||
/**
|
||||
* (optional) The metadata attribute name to provide the thumbnail alt.
|
||||
*
|
||||
* "Thumbnail" will be used if the attribute name is not provided.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public $thumbnail_alt_metadata_attribute;
|
||||
|
||||
/**
|
||||
* (optional) The metadata attribute name to provide the thumbnail title.
|
||||
*
|
||||
* The name of the media item will be used if the attribute name is not
|
||||
* provided.
|
||||
*
|
||||
* @var string|null
|
||||
*/
|
||||
public $thumbnail_title_metadata_attribute;
|
||||
|
||||
/**
|
||||
* The metadata attribute name to provide the default name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $default_name_metadata_attribute = 'default_name';
|
||||
|
||||
}
|
||||
@@ -0,0 +1,143 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media\Controller;
|
||||
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||
use Drupal\Core\Entity\ContentEntityStorageInterface;
|
||||
use Drupal\Core\Entity\EntityRepositoryInterface;
|
||||
use Drupal\Core\Render\RendererInterface;
|
||||
use Drupal\filter\FilterFormatInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpFoundation\Response;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
|
||||
/**
|
||||
* Controller which renders a preview of the provided text.
|
||||
*
|
||||
* @internal
|
||||
* This is an internal part of the media system in Drupal core and may be
|
||||
* subject to change in minor releases. This class should not be
|
||||
* instantiated or extended by external code.
|
||||
*/
|
||||
class MediaFilterController implements ContainerInjectionInterface {
|
||||
|
||||
/**
|
||||
* The renderer service.
|
||||
*
|
||||
* @var \Drupal\Core\Render\RendererInterface
|
||||
*/
|
||||
protected $renderer;
|
||||
|
||||
/**
|
||||
* The media storage.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\ContentEntityStorageInterface
|
||||
*/
|
||||
protected $mediaStorage;
|
||||
|
||||
/**
|
||||
* The entity repository.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityRepositoryInterface
|
||||
*/
|
||||
protected $entityRepository;
|
||||
|
||||
/**
|
||||
* Constructs an MediaFilterController instance.
|
||||
*
|
||||
* @param \Drupal\Core\Render\RendererInterface $renderer
|
||||
* The renderer service.
|
||||
* @param \Drupal\Core\Entity\ContentEntityStorageInterface $media_storage
|
||||
* The media storage.
|
||||
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
|
||||
* The entity repository.
|
||||
*/
|
||||
public function __construct(RendererInterface $renderer, ContentEntityStorageInterface $media_storage, EntityRepositoryInterface $entity_repository) {
|
||||
$this->renderer = $renderer;
|
||||
$this->mediaStorage = $media_storage;
|
||||
$this->entityRepository = $entity_repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('renderer'),
|
||||
$container->get('entity_type.manager')->getStorage('media'),
|
||||
$container->get('entity.repository')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a HTML response containing a preview of the text after filtering.
|
||||
*
|
||||
* Applies all of the given text format's filters, not just the `media_embed`
|
||||
* filter, because for example `filter_align` and `filter_caption` may apply
|
||||
* to it as well.
|
||||
*
|
||||
* @param \Symfony\Component\HttpFoundation\Request $request
|
||||
* The request object.
|
||||
* @param \Drupal\filter\FilterFormatInterface $filter_format
|
||||
* The text format.
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
* The filtered text.
|
||||
*
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\NotFoundHttpException
|
||||
* Throws an exception if 'text' parameter is not found in the query
|
||||
* string.
|
||||
*
|
||||
* @see \Drupal\editor\EditorController::getUntransformedText
|
||||
*/
|
||||
public function preview(Request $request, FilterFormatInterface $filter_format) {
|
||||
$text = $request->query->get('text');
|
||||
$uuid = $request->query->get('uuid');
|
||||
if ($text == '' || $uuid == '') {
|
||||
throw new NotFoundHttpException();
|
||||
}
|
||||
|
||||
$build = [
|
||||
'#type' => 'processed_text',
|
||||
'#text' => $text,
|
||||
'#format' => $filter_format->id(),
|
||||
];
|
||||
$html = $this->renderer->renderPlain($build);
|
||||
|
||||
// Load the media item so we can embed the label in the response, for use
|
||||
// in an ARIA label.
|
||||
$headers = [];
|
||||
if ($media = $this->entityRepository->loadEntityByUuid('media', $uuid)) {
|
||||
$headers['Drupal-Media-Label'] = $this->entityRepository->getTranslationFromContext($media)->label();
|
||||
}
|
||||
|
||||
// Note that we intentionally do not use:
|
||||
// - \Drupal\Core\Cache\CacheableResponse because caching it on the server
|
||||
// side is wasteful, hence there is no need for cacheability metadata.
|
||||
// - \Drupal\Core\Render\HtmlResponse because there is no need for
|
||||
// attachments nor cacheability metadata.
|
||||
return (new Response($html, 200, $headers))
|
||||
// Do not allow any intermediary to cache the response, only the end user.
|
||||
->setPrivate()
|
||||
// Allow the end user to cache it for up to 5 minutes.
|
||||
->setMaxAge(300);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks access based on media_embed filter status on the text format.
|
||||
*
|
||||
* @param \Drupal\filter\FilterFormatInterface $filter_format
|
||||
* The text format for which to check access.
|
||||
*
|
||||
* @return \Drupal\Core\Access\AccessResultInterface
|
||||
* The access result.
|
||||
*/
|
||||
public static function formatUsesMediaEmbedFilter(FilterFormatInterface $filter_format) {
|
||||
$filters = $filter_format->filters();
|
||||
return AccessResult::allowedIf($filters->has('media_embed') && $filters->get('media_embed')->status)
|
||||
->addCacheableDependency($filter_format);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,195 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media\Controller;
|
||||
|
||||
use Drupal\Component\Utility\Crypt;
|
||||
use Drupal\Core\Cache\CacheableMetadata;
|
||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||
use Drupal\Core\Render\HtmlResponse;
|
||||
use Drupal\Core\Render\RenderContext;
|
||||
use Drupal\Core\Render\RendererInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\media\IFrameMarkup;
|
||||
use Drupal\media\IFrameUrlHelper;
|
||||
use Drupal\media\OEmbed\ResourceException;
|
||||
use Drupal\media\OEmbed\ResourceFetcherInterface;
|
||||
use Drupal\media\OEmbed\UrlResolverInterface;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\HttpFoundation\Request;
|
||||
use Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException;
|
||||
|
||||
/**
|
||||
* Controller which renders an oEmbed resource in a bare page (without blocks).
|
||||
*
|
||||
* This controller is meant to render untrusted third-party HTML returned by
|
||||
* an oEmbed provider in an iframe, so as to mitigate the potential dangers of
|
||||
* of displaying third-party markup (i.e., XSS). The HTML returned by this
|
||||
* controller should not be trusted, and should *never* be displayed outside
|
||||
* of an iframe.
|
||||
*
|
||||
* @internal
|
||||
* This is an internal part of the media system in Drupal core and may be
|
||||
* subject to change in minor releases. This class should not be
|
||||
* instantiated or extended by external code.
|
||||
*/
|
||||
class OEmbedIframeController implements ContainerInjectionInterface {
|
||||
|
||||
/**
|
||||
* The oEmbed resource fetcher service.
|
||||
*
|
||||
* @var \Drupal\media\OEmbed\ResourceFetcherInterface
|
||||
*/
|
||||
protected $resourceFetcher;
|
||||
|
||||
/**
|
||||
* The oEmbed URL resolver service.
|
||||
*
|
||||
* @var \Drupal\media\OEmbed\UrlResolverInterface
|
||||
*/
|
||||
protected $urlResolver;
|
||||
|
||||
/**
|
||||
* The renderer service.
|
||||
*
|
||||
* @var \Drupal\Core\Render\RendererInterface
|
||||
*/
|
||||
protected $renderer;
|
||||
|
||||
/**
|
||||
* The logger channel.
|
||||
*
|
||||
* @var \Psr\Log\LoggerInterface
|
||||
*/
|
||||
protected $logger;
|
||||
|
||||
/**
|
||||
* The iFrame URL helper service.
|
||||
*
|
||||
* @var \Drupal\media\IFrameUrlHelper
|
||||
*/
|
||||
protected $iFrameUrlHelper;
|
||||
|
||||
/**
|
||||
* Constructs an OEmbedIframeController instance.
|
||||
*
|
||||
* @param \Drupal\media\OEmbed\ResourceFetcherInterface $resource_fetcher
|
||||
* The oEmbed resource fetcher service.
|
||||
* @param \Drupal\media\OEmbed\UrlResolverInterface $url_resolver
|
||||
* The oEmbed URL resolver service.
|
||||
* @param \Drupal\Core\Render\RendererInterface $renderer
|
||||
* The renderer service.
|
||||
* @param \Psr\Log\LoggerInterface $logger
|
||||
* The logger channel.
|
||||
* @param \Drupal\media\IFrameUrlHelper $iframe_url_helper
|
||||
* The iFrame URL helper service.
|
||||
*/
|
||||
public function __construct(ResourceFetcherInterface $resource_fetcher, UrlResolverInterface $url_resolver, RendererInterface $renderer, LoggerInterface $logger, IFrameUrlHelper $iframe_url_helper) {
|
||||
$this->resourceFetcher = $resource_fetcher;
|
||||
$this->urlResolver = $url_resolver;
|
||||
$this->renderer = $renderer;
|
||||
$this->logger = $logger;
|
||||
$this->iFrameUrlHelper = $iframe_url_helper;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('media.oembed.resource_fetcher'),
|
||||
$container->get('media.oembed.url_resolver'),
|
||||
$container->get('renderer'),
|
||||
$container->get('logger.factory')->get('media'),
|
||||
$container->get('media.oembed.iframe_url_helper')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders an oEmbed resource.
|
||||
*
|
||||
* @param \Symfony\Component\HttpFoundation\Request $request
|
||||
* The request object.
|
||||
*
|
||||
* @return \Symfony\Component\HttpFoundation\Response
|
||||
* The response object.
|
||||
*
|
||||
* @throws \Symfony\Component\HttpKernel\Exception\AccessDeniedHttpException
|
||||
* Will be thrown if the 'hash' parameter does not match the expected hash
|
||||
* of the 'url' parameter.
|
||||
*/
|
||||
public function render(Request $request) {
|
||||
$url = $request->query->get('url');
|
||||
$max_width = $request->query->getInt('max_width', NULL);
|
||||
$max_height = $request->query->getInt('max_height', NULL);
|
||||
|
||||
// Hash the URL and max dimensions, and ensure it is equal to the hash
|
||||
// parameter passed in the query string.
|
||||
$hash = $this->iFrameUrlHelper->getHash($url, $max_width, $max_height);
|
||||
if (!hash_equals($hash, $request->query->get('hash', ''))) {
|
||||
throw new AccessDeniedHttpException('This resource is not available');
|
||||
}
|
||||
|
||||
// Return a response instead of a render array so that the frame content
|
||||
// will not have all the blocks and page elements normally rendered by
|
||||
// Drupal.
|
||||
$response = new HtmlResponse();
|
||||
$response->addCacheableDependency(Url::createFromRequest($request));
|
||||
|
||||
try {
|
||||
$resource_url = $this->urlResolver->getResourceUrl($url, $max_width, $max_height);
|
||||
$resource = $this->resourceFetcher->fetchResource($resource_url);
|
||||
|
||||
$placeholder_token = Crypt::randomBytesBase64(55);
|
||||
|
||||
// Render the content in a new render context so that the cacheability
|
||||
// metadata of the rendered HTML will be captured correctly.
|
||||
$element = [
|
||||
'#theme' => 'media_oembed_iframe',
|
||||
// Even though the resource HTML is untrusted, IFrameMarkup::create()
|
||||
// will create a trusted string. The only reason this is okay is
|
||||
// because we are serving it in an iframe, which will mitigate the
|
||||
// potential dangers of displaying third-party markup.
|
||||
'#media' => IFrameMarkup::create($resource->getHtml()),
|
||||
'#cache' => [
|
||||
// Add the 'rendered' cache tag as this response is not processed by
|
||||
// \Drupal\Core\Render\MainContent\HtmlRenderer::renderResponse().
|
||||
'tags' => ['rendered'],
|
||||
],
|
||||
'#attached' => [
|
||||
'html_response_attachment_placeholders' => [
|
||||
'styles' => '<css-placeholder token="' . $placeholder_token . '">',
|
||||
],
|
||||
'library' => [
|
||||
'media/oembed.frame',
|
||||
],
|
||||
],
|
||||
'#placeholder_token' => $placeholder_token,
|
||||
];
|
||||
$content = $this->renderer->executeInRenderContext(new RenderContext(), function () use ($resource, $element) {
|
||||
return $this->renderer->render($element);
|
||||
});
|
||||
$response
|
||||
->setContent($content)
|
||||
->setAttachments($element['#attached'])
|
||||
->addCacheableDependency($resource)
|
||||
->addCacheableDependency(CacheableMetadata::createFromRenderArray($element));
|
||||
}
|
||||
catch (ResourceException $e) {
|
||||
// Prevent the response from being cached.
|
||||
$response->setMaxAge(0);
|
||||
|
||||
// The oEmbed system makes heavy use of exception wrapping, so log the
|
||||
// entire exception chain to help with troubleshooting.
|
||||
do {
|
||||
// @todo Log additional information from ResourceException, to help with
|
||||
// debugging, in https://www.drupal.org/project/drupal/issues/2972846.
|
||||
$this->logger->error($e->getMessage());
|
||||
$e = $e->getPrevious();
|
||||
} while ($e);
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,533 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media\Entity;
|
||||
|
||||
use Drupal\Core\Entity\EditorialContentEntityBase;
|
||||
use Drupal\Core\Entity\EntityStorageInterface;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Field\BaseFieldDefinition;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Drupal\media\MediaInterface;
|
||||
use Drupal\media\MediaSourceEntityConstraintsInterface;
|
||||
use Drupal\media\MediaSourceFieldConstraintsInterface;
|
||||
use Drupal\user\EntityOwnerTrait;
|
||||
|
||||
/**
|
||||
* Defines the media entity class.
|
||||
*
|
||||
* @todo Remove default/fallback entity form operation when #2006348 is done.
|
||||
* @see https://www.drupal.org/node/2006348.
|
||||
*
|
||||
* @ContentEntityType(
|
||||
* id = "media",
|
||||
* label = @Translation("Media"),
|
||||
* label_singular = @Translation("media item"),
|
||||
* label_plural = @Translation("media items"),
|
||||
* label_count = @PluralTranslation(
|
||||
* singular = "@count media item",
|
||||
* plural = "@count media items"
|
||||
* ),
|
||||
* bundle_label = @Translation("Media type"),
|
||||
* handlers = {
|
||||
* "storage" = "Drupal\media\MediaStorage",
|
||||
* "view_builder" = "Drupal\Core\Entity\EntityViewBuilder",
|
||||
* "list_builder" = "Drupal\media\MediaListBuilder",
|
||||
* "access" = "Drupal\media\MediaAccessControlHandler",
|
||||
* "form" = {
|
||||
* "default" = "Drupal\media\MediaForm",
|
||||
* "add" = "Drupal\media\MediaForm",
|
||||
* "edit" = "Drupal\media\MediaForm",
|
||||
* "delete" = "Drupal\Core\Entity\ContentEntityDeleteForm",
|
||||
* "delete-multiple-confirm" = "Drupal\Core\Entity\Form\DeleteMultipleForm",
|
||||
* },
|
||||
* "views_data" = "Drupal\media\MediaViewsData",
|
||||
* "route_provider" = {
|
||||
* "html" = "Drupal\media\Routing\MediaRouteProvider",
|
||||
* }
|
||||
* },
|
||||
* base_table = "media",
|
||||
* data_table = "media_field_data",
|
||||
* revision_table = "media_revision",
|
||||
* revision_data_table = "media_field_revision",
|
||||
* translatable = TRUE,
|
||||
* show_revision_ui = TRUE,
|
||||
* entity_keys = {
|
||||
* "id" = "mid",
|
||||
* "revision" = "vid",
|
||||
* "bundle" = "bundle",
|
||||
* "label" = "name",
|
||||
* "langcode" = "langcode",
|
||||
* "uuid" = "uuid",
|
||||
* "published" = "status",
|
||||
* "owner" = "uid",
|
||||
* },
|
||||
* revision_metadata_keys = {
|
||||
* "revision_user" = "revision_user",
|
||||
* "revision_created" = "revision_created",
|
||||
* "revision_log_message" = "revision_log_message",
|
||||
* },
|
||||
* bundle_entity_type = "media_type",
|
||||
* permission_granularity = "bundle",
|
||||
* admin_permission = "administer media",
|
||||
* field_ui_base_route = "entity.media_type.edit_form",
|
||||
* common_reference_target = TRUE,
|
||||
* links = {
|
||||
* "add-page" = "/media/add",
|
||||
* "add-form" = "/media/add/{media_type}",
|
||||
* "canonical" = "/media/{media}/edit",
|
||||
* "collection" = "/admin/content/media",
|
||||
* "delete-form" = "/media/{media}/delete",
|
||||
* "delete-multiple-form" = "/media/delete",
|
||||
* "edit-form" = "/media/{media}/edit",
|
||||
* "revision" = "/media/{media}/revisions/{media_revision}/view",
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
class Media extends EditorialContentEntityBase implements MediaInterface {
|
||||
|
||||
use EntityOwnerTrait;
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getName() {
|
||||
$name = $this->getEntityKey('label');
|
||||
|
||||
if (empty($name)) {
|
||||
$media_source = $this->getSource();
|
||||
return $media_source->getMetadata($this, $media_source->getPluginDefinition()['default_name_metadata_attribute']);
|
||||
}
|
||||
|
||||
return $name;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function label() {
|
||||
return $this->getName();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setName($name) {
|
||||
return $this->set('name', $name);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCreatedTime() {
|
||||
return $this->get('created')->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function setCreatedTime($timestamp) {
|
||||
return $this->set('created', $timestamp);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@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.
|
||||
*
|
||||
* @internal
|
||||
*
|
||||
* @todo There has been some disagreement about how to handle updates to
|
||||
* thumbnails. We need to decide on what the API will be for this.
|
||||
* https://www.drupal.org/node/2878119
|
||||
*/
|
||||
protected function updateThumbnail($from_queue = FALSE) {
|
||||
$this->thumbnail->target_id = $this->loadThumbnail($this->getThumbnailUri($from_queue))->id();
|
||||
|
||||
// Set the thumbnail alt.
|
||||
$media_source = $this->getSource();
|
||||
$plugin_definition = $media_source->getPluginDefinition();
|
||||
|
||||
$this->thumbnail->alt = '';
|
||||
if (!empty($plugin_definition['thumbnail_alt_metadata_attribute'])) {
|
||||
$this->thumbnail->alt = $media_source->getMetadata($this, $plugin_definition['thumbnail_alt_metadata_attribute']);
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Loads the file entity for the thumbnail.
|
||||
*
|
||||
* If the file entity does not exist, it will be created.
|
||||
*
|
||||
* @param string $thumbnail_uri
|
||||
* (optional) The URI of the thumbnail, used to load or create the file
|
||||
* entity. If omitted, the default thumbnail URI will be used.
|
||||
*
|
||||
* @return \Drupal\file\FileInterface
|
||||
* The thumbnail file entity.
|
||||
*/
|
||||
protected function loadThumbnail($thumbnail_uri = NULL) {
|
||||
$values = [
|
||||
'uri' => $thumbnail_uri ?: $this->getDefaultThumbnailUri(),
|
||||
];
|
||||
|
||||
$file_storage = $this->entityTypeManager()->getStorage('file');
|
||||
|
||||
$existing = $file_storage->loadByProperties($values);
|
||||
if ($existing) {
|
||||
$file = reset($existing);
|
||||
}
|
||||
else {
|
||||
/** @var \Drupal\file\FileInterface $file */
|
||||
$file = $file_storage->create($values);
|
||||
if ($owner = $this->getOwner()) {
|
||||
$file->setOwner($owner);
|
||||
}
|
||||
$file->setPermanent();
|
||||
$file->save();
|
||||
}
|
||||
return $file;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URI of the default thumbnail.
|
||||
*
|
||||
* @return string
|
||||
* The default thumbnail URI.
|
||||
*/
|
||||
protected function getDefaultThumbnailUri() {
|
||||
$default_thumbnail_filename = $this->getSource()->getPluginDefinition()['default_thumbnail_filename'];
|
||||
return \Drupal::config('media.settings')->get('icon_base_uri') . '/' . $default_thumbnail_filename;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 the URI for the thumbnail of a media item.
|
||||
*
|
||||
* If thumbnail fetching is queued, new media items will use the default
|
||||
* thumbnail, and existing media items will use the current thumbnail, until
|
||||
* the queue is processed and the updated thumbnail has been fetched.
|
||||
* Otherwise, the new thumbnail will be fetched immediately.
|
||||
*
|
||||
* @param bool $from_queue
|
||||
* Specifies whether the thumbnail is being fetched from the queue.
|
||||
*
|
||||
* @return string
|
||||
* The file URI for the thumbnail of the media item.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
protected function getThumbnailUri($from_queue) {
|
||||
$thumbnails_queued = $this->bundle->entity->thumbnailDownloadsAreQueued();
|
||||
if ($thumbnails_queued && $this->isNew()) {
|
||||
return $this->getDefaultThumbnailUri();
|
||||
}
|
||||
elseif ($thumbnails_queued && !$from_queue) {
|
||||
return $this->get('thumbnail')->entity->getFileUri();
|
||||
}
|
||||
|
||||
$source = $this->getSource();
|
||||
return $source->getMetadata($this, $source->getPluginDefinition()['thumbnail_uri_metadata_attribute']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Determines if the source field value has changed.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the source field value changed, FALSE otherwise.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
protected function hasSourceFieldChanged() {
|
||||
$source_field_name = $this->getSource()->getConfiguration()['source_field'];
|
||||
$current_items = $this->get($source_field_name);
|
||||
return isset($this->original) && !$current_items->equals($this->original->get($source_field_name));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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->hasSourceFieldChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function preSave(EntityStorageInterface $storage) {
|
||||
parent::preSave($storage);
|
||||
|
||||
// If no thumbnail has been explicitly set, use the default thumbnail.
|
||||
if ($this->get('thumbnail')->isEmpty()) {
|
||||
$this->thumbnail->target_id = $this->loadThumbnail()->id();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the media entity's field values from the source's metadata.
|
||||
*
|
||||
* Fetching the metadata could be slow (e.g., if requesting it from a remote
|
||||
* API), so this is called by \Drupal\media\MediaStorage::save() prior to it
|
||||
* beginning the database transaction, whereas static::preSave() executes
|
||||
* after the transaction has already started.
|
||||
*
|
||||
* @internal
|
||||
* Expose this as an API in
|
||||
* https://www.drupal.org/project/drupal/issues/2992426.
|
||||
*/
|
||||
public function prepareSave() {
|
||||
// @todo If the source plugin talks to a remote API (e.g. oEmbed), this code
|
||||
// might be performing a fair number of HTTP requests. This is dangerously
|
||||
// brittle and should probably be handled by a queue, to avoid doing HTTP
|
||||
// operations during entity save. See
|
||||
// https://www.drupal.org/project/drupal/issues/2976875 for more.
|
||||
|
||||
// In order for metadata to be mapped correctly, $this->original must be
|
||||
// set. However, that is only set once parent::save() is called, so work
|
||||
// around that by setting it here.
|
||||
if (!isset($this->original) && $id = $this->id()) {
|
||||
$this->original = $this->entityTypeManager()
|
||||
->getStorage('media')
|
||||
->loadUnchanged($id);
|
||||
}
|
||||
|
||||
$media_source = $this->getSource();
|
||||
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->hasSourceFieldChanged())) {
|
||||
$translation->set($entity_field_name, $media_source->getMetadata($translation, $metadata_attribute_name));
|
||||
}
|
||||
}
|
||||
|
||||
// Try to set a default name for this media item if no name is provided.
|
||||
if ($translation->get('name')->isEmpty()) {
|
||||
$translation->setName($translation->getName());
|
||||
}
|
||||
|
||||
// Set thumbnail.
|
||||
if ($translation->shouldUpdateThumbnail($this->isNew())) {
|
||||
$translation->updateThumbnail();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@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::ownerBaseFieldDefinitions($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)
|
||||
->setDisplayConfigurable('view', TRUE);
|
||||
|
||||
$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']
|
||||
->setLabel(t('Authored by'))
|
||||
->setDescription(t('The user ID of the author.'))
|
||||
->setRevisionable(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['status']
|
||||
->setDisplayOptions('form', [
|
||||
'type' => 'boolean_checkbox',
|
||||
'settings' => [
|
||||
'display_label' => TRUE,
|
||||
],
|
||||
'weight' => 100,
|
||||
])
|
||||
->setDisplayConfigurable('form', 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()
|
||||
*
|
||||
* @deprecated The ::getCurrentUserId method is deprecated in 8.6.x and will
|
||||
* be removed before 9.0.0.
|
||||
*
|
||||
* @return int[]
|
||||
* An array of default values.
|
||||
*/
|
||||
public static function getCurrentUserId() {
|
||||
@trigger_error('The ::getCurrentUserId method is deprecated in 8.6.x and will be removed before 9.0.0.', E_USER_DEPRECATED);
|
||||
return [\Drupal::currentUser()->id()];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function getRequestTime() {
|
||||
return \Drupal::time()->getRequestTime();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,238 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media\Entity;
|
||||
|
||||
use Drupal\Core\Config\Entity\ConfigEntityBundleBase;
|
||||
use Drupal\Core\Entity\EntityWithPluginCollectionInterface;
|
||||
use Drupal\Core\Plugin\DefaultSingleLazyPluginCollection;
|
||||
use Drupal\media\MediaTypeInterface;
|
||||
|
||||
/**
|
||||
* Defines the Media type configuration entity.
|
||||
*
|
||||
* @ConfigEntityType(
|
||||
* id = "media_type",
|
||||
* label = @Translation("Media type"),
|
||||
* label_collection = @Translation("Media types"),
|
||||
* label_singular = @Translation("media type"),
|
||||
* label_plural = @Translation("media types"),
|
||||
* label_count = @PluralTranslation(
|
||||
* singular = "@count media type",
|
||||
* plural = "@count media types"
|
||||
* ),
|
||||
* handlers = {
|
||||
* "access" = "Drupal\media\MediaTypeAccessControlHandler",
|
||||
* "form" = {
|
||||
* "add" = "Drupal\media\MediaTypeForm",
|
||||
* "edit" = "Drupal\media\MediaTypeForm",
|
||||
* "delete" = "Drupal\media\Form\MediaTypeDeleteConfirmForm"
|
||||
* },
|
||||
* "list_builder" = "Drupal\media\MediaTypeListBuilder",
|
||||
* "route_provider" = {
|
||||
* "html" = "Drupal\Core\Entity\Routing\DefaultHtmlRouteProvider",
|
||||
* }
|
||||
* },
|
||||
* admin_permission = "administer media types",
|
||||
* config_prefix = "type",
|
||||
* bundle_of = "media",
|
||||
* entity_keys = {
|
||||
* "id" = "id",
|
||||
* "label" = "label",
|
||||
* "status" = "status",
|
||||
* },
|
||||
* config_export = {
|
||||
* "id",
|
||||
* "label",
|
||||
* "description",
|
||||
* "source",
|
||||
* "queue_thumbnail_downloads",
|
||||
* "new_revision",
|
||||
* "source_configuration",
|
||||
* "field_map",
|
||||
* "status",
|
||||
* },
|
||||
* links = {
|
||||
* "add-form" = "/admin/structure/media/add",
|
||||
* "edit-form" = "/admin/structure/media/manage/{media_type}",
|
||||
* "delete-form" = "/admin/structure/media/manage/{media_type}/delete",
|
||||
* "collection" = "/admin/structure/media",
|
||||
* },
|
||||
* )
|
||||
*/
|
||||
class MediaType extends ConfigEntityBundleBase implements MediaTypeInterface, EntityWithPluginCollectionInterface {
|
||||
|
||||
/**
|
||||
* The machine name of this media type.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $id;
|
||||
|
||||
/**
|
||||
* The human-readable name of the media type.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $label;
|
||||
|
||||
/**
|
||||
* A brief description of this media type.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $description;
|
||||
|
||||
/**
|
||||
* The media source ID.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $source;
|
||||
|
||||
/**
|
||||
* Whether media items should be published by default.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $status = TRUE;
|
||||
|
||||
/**
|
||||
* Whether thumbnail downloads are queued.
|
||||
*
|
||||
* @var bool
|
||||
*
|
||||
* @see \Drupal\media\MediaTypeInterface::thumbnailDownloadsAreQueued()
|
||||
*/
|
||||
protected $queue_thumbnail_downloads = FALSE;
|
||||
|
||||
/**
|
||||
* Default value of the 'Create new revision' checkbox of this media type.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $new_revision = FALSE;
|
||||
|
||||
/**
|
||||
* The media source configuration.
|
||||
*
|
||||
* A media source can provide a configuration form with source plugin-specific
|
||||
* configuration settings, which must at least include a source_field element
|
||||
* containing a the name of the source field for the media type. The source
|
||||
* configuration is defined by, and used to load, the source plugin. See
|
||||
* \Drupal\media\MediaTypeInterface for an explanation of media sources.
|
||||
*
|
||||
* @var array
|
||||
*
|
||||
* @see \Drupal\media\MediaTypeInterface::getSource()
|
||||
*/
|
||||
protected $source_configuration = [];
|
||||
|
||||
/**
|
||||
* Lazy collection for the media source.
|
||||
*
|
||||
* @var \Drupal\Core\Plugin\DefaultSingleLazyPluginCollection
|
||||
*/
|
||||
protected $sourcePluginCollection;
|
||||
|
||||
/**
|
||||
* The metadata field map.
|
||||
*
|
||||
* @var array
|
||||
*
|
||||
* @see \Drupal\media\MediaTypeInterface::getFieldMap()
|
||||
*/
|
||||
protected $field_map = [];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getPluginCollections() {
|
||||
return [
|
||||
'source_configuration' => $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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media\EventSubscriber;
|
||||
|
||||
use Drupal\Core\Cache\CacheTagsInvalidatorInterface;
|
||||
use Drupal\Core\Config\ConfigCrudEvent;
|
||||
use Drupal\Core\Config\ConfigEvents;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Routing\RouteBuilderInterface;
|
||||
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
|
||||
|
||||
/**
|
||||
* Listens to the config save event for media.settings.
|
||||
*/
|
||||
class MediaConfigSubscriber implements EventSubscriberInterface {
|
||||
|
||||
/**
|
||||
* The route builder.
|
||||
*
|
||||
* @var \Drupal\Core\Routing\RouteBuilderInterface
|
||||
*/
|
||||
protected $routeBuilder;
|
||||
|
||||
/**
|
||||
* The cache tags invalidator.
|
||||
*
|
||||
* @var \Drupal\Core\Cache\CacheTagsInvalidatorInterface
|
||||
*/
|
||||
protected $cacheTagsInvalidator;
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* Constructs the MediaConfigSubscriber.
|
||||
*
|
||||
* @param \Drupal\Core\Routing\RouteBuilderInterface $router_builder
|
||||
* The route builder.
|
||||
* @param \Drupal\Core\Cache\CacheTagsInvalidatorInterface $cache_tags_invalidator
|
||||
* The cache tags invalidator.
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
*/
|
||||
public function __construct(RouteBuilderInterface $router_builder, CacheTagsInvalidatorInterface $cache_tags_invalidator, EntityTypeManagerInterface $entity_type_manager) {
|
||||
$this->routeBuilder = $router_builder;
|
||||
$this->cacheTagsInvalidator = $cache_tags_invalidator;
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates entity type definitions and ensures routes are rebuilt when needed.
|
||||
*
|
||||
* @param \Drupal\Core\Config\ConfigCrudEvent $event
|
||||
* The ConfigCrudEvent to process.
|
||||
*/
|
||||
public function onSave(ConfigCrudEvent $event) {
|
||||
$saved_config = $event->getConfig();
|
||||
if ($saved_config->getName() === 'media.settings' && $event->isChanged('standalone_url')) {
|
||||
$this->cacheTagsInvalidator->invalidateTags([
|
||||
// The configuration change triggers entity type definition changes,
|
||||
// which in turn triggers routes to appear or disappear.
|
||||
// @see media_entity_type_alter()
|
||||
'entity_types',
|
||||
// The 'rendered' cache tag needs to be explicitly invalidated to ensure
|
||||
// that all links to Media entities are re-rendered. Ideally, this would
|
||||
// not be necessary; invalidating the 'entity_types' cache tag should be
|
||||
// sufficient. But that cache tag would then need to be on nearly
|
||||
// everything, resulting in excessive complexity. We prefer pragmatism.
|
||||
'rendered',
|
||||
]);
|
||||
// @todo Remove this when invalidating the 'entity_types' cache tag is
|
||||
// respected by the entity type plugin manager. See
|
||||
// https://www.drupal.org/project/drupal/issues/3001284 and
|
||||
// https://www.drupal.org/project/drupal/issues/3013659.
|
||||
$this->entityTypeManager->clearCachedDefinitions();
|
||||
$this->routeBuilder->setRebuildNeeded();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function getSubscribedEvents() {
|
||||
$events[ConfigEvents::SAVE][] = ['onSave'];
|
||||
return $events;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,329 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media\Form;
|
||||
|
||||
use Drupal\Core\Entity\EntityRepositoryInterface;
|
||||
use Drupal\Core\Form\FormBase;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\editor\EditorInterface;
|
||||
use Drupal\Core\Ajax\AjaxResponse;
|
||||
use Drupal\Core\Ajax\HtmlCommand;
|
||||
use Drupal\editor\Ajax\EditorDialogSave;
|
||||
use Drupal\Core\Ajax\CloseModalDialogCommand;
|
||||
use Drupal\filter\Plugin\FilterInterface;
|
||||
use Drupal\image\Plugin\Field\FieldType\ImageItem;
|
||||
use Drupal\media\MediaInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Drupal\Core\Entity\EntityDisplayRepositoryInterface;
|
||||
|
||||
/**
|
||||
* Provides a media embed dialog for text editors.
|
||||
*
|
||||
* Depending on the configuration of the filters associated with the text
|
||||
* editor, this dialog allows users to set the alt text, alignment, and
|
||||
* captioning status for embedded media items.
|
||||
*
|
||||
* @internal
|
||||
* This is an internal part of the media system in Drupal core and may be
|
||||
* subject to change in minor releases. This class should not be
|
||||
* instantiated or extended by external code.
|
||||
*/
|
||||
class EditorMediaDialog extends FormBase {
|
||||
|
||||
/**
|
||||
* The entity repository.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityRepositoryInterface
|
||||
*/
|
||||
protected $entityRepository;
|
||||
|
||||
/**
|
||||
* The entity display repository.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface
|
||||
*/
|
||||
protected $entityDisplayRepository;
|
||||
|
||||
/**
|
||||
* Constructs a EditorMediaDialog object.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
|
||||
* The entity repository.
|
||||
* @param \Drupal\Core\Entity\EntityDisplayRepositoryInterface $entity_display_repository
|
||||
* The entity display repository.
|
||||
*/
|
||||
public function __construct(EntityRepositoryInterface $entity_repository, EntityDisplayRepositoryInterface $entity_display_repository) {
|
||||
$this->entityRepository = $entity_repository;
|
||||
$this->entityDisplayRepository = $entity_display_repository;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('entity.repository'),
|
||||
$container->get('entity_display.repository')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFormId() {
|
||||
return 'editor_media_dialog';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @param \Drupal\editor\EditorInterface $editor
|
||||
* The text editor to which this dialog corresponds.
|
||||
*/
|
||||
public function buildForm(array $form, FormStateInterface $form_state, EditorInterface $editor = NULL) {
|
||||
// This form is special, in that the default values do not come from the
|
||||
// server side, but from the client side, from a text editor. We must cache
|
||||
// this data in form state, because when the form is rebuilt, we will be
|
||||
// receiving values from the form, instead of the values from the text
|
||||
// editor. If we don't cache it, this data will be lost. By convention,
|
||||
// the data that the text editor sends to any dialog is in the
|
||||
// 'editor_object' key.
|
||||
if (isset($form_state->getUserInput()['editor_object'])) {
|
||||
$editor_object = $form_state->getUserInput()['editor_object'];
|
||||
// The data that the text editor sends to any dialog is in
|
||||
// the 'editor_object' key.
|
||||
// @see core/modules/ckeditor/js/ckeditor.es6.js
|
||||
$media_embed_element = $editor_object['attributes'];
|
||||
$form_state->set('media_embed_element', $media_embed_element);
|
||||
$has_caption = $editor_object['hasCaption'];
|
||||
$form_state
|
||||
->set('hasCaption', $has_caption)
|
||||
->setCached(TRUE);
|
||||
}
|
||||
else {
|
||||
// Retrieve the user input from form state.
|
||||
$media_embed_element = $form_state->get('media_embed_element');
|
||||
$has_caption = $form_state->get('hasCaption');
|
||||
}
|
||||
|
||||
$form['#tree'] = TRUE;
|
||||
$form['#attached']['library'][] = 'editor/drupal.editor.dialog';
|
||||
$form['#prefix'] = '<div id="editor-media-dialog-form">';
|
||||
$form['#suffix'] = '</div>';
|
||||
|
||||
$filters = $editor->getFilterFormat()->filters();
|
||||
$filter_html = $filters->get('filter_html');
|
||||
$filter_align = $filters->get('filter_align');
|
||||
$filter_caption = $filters->get('filter_caption');
|
||||
$media_embed_filter = $filters->get('media_embed');
|
||||
|
||||
$allowed_attributes = [];
|
||||
if ($filter_html->status) {
|
||||
$restrictions = $filter_html->getHTMLRestrictions();
|
||||
$allowed_attributes = $restrictions['allowed']['drupal-media'];
|
||||
}
|
||||
|
||||
$media = $this->entityRepository->loadEntityByUuid('media', $media_embed_element['data-entity-uuid']);
|
||||
|
||||
if ($image_field_name = $this->getMediaImageSourceFieldName($media)) {
|
||||
// We'll want the alt text from the same language as the host.
|
||||
if (!empty($editor_object['hostEntityLangcode']) && $media->hasTranslation($editor_object['hostEntityLangcode'])) {
|
||||
$media = $media->getTranslation($editor_object['hostEntityLangcode']);
|
||||
}
|
||||
$settings = $media->{$image_field_name}->getItemDefinition()->getSettings();
|
||||
$alt = isset($media_embed_element['alt']) ? $media_embed_element['alt'] : NULL;
|
||||
$form['alt'] = [
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('Alternate text'),
|
||||
'#default_value' => $alt,
|
||||
'#description' => $this->t('Short description of the image used by screen readers and displayed when the image is not loaded. This is important for accessibility.'),
|
||||
'#required_error' => $this->t('Alternative text is required.<br />(Only in rare cases should this be left empty. To create empty alternative text, enter <code>""</code> — two double quotes without any content).'),
|
||||
'#maxlength' => 2048,
|
||||
'#placeholder' => $media->{$image_field_name}->alt,
|
||||
'#parents' => ['attributes', 'alt'],
|
||||
'#access' => !empty($settings['alt_field']) && ($filter_html->status === FALSE || !empty($allowed_attributes['alt'])),
|
||||
];
|
||||
}
|
||||
|
||||
// When Drupal core's filter_align is being used, the text editor offers the
|
||||
// ability to change the alignment.
|
||||
$form['align'] = [
|
||||
'#title' => $this->t('Align'),
|
||||
'#type' => 'radios',
|
||||
'#options' => [
|
||||
'none' => $this->t('None'),
|
||||
'left' => $this->t('Left'),
|
||||
'center' => $this->t('Center'),
|
||||
'right' => $this->t('Right'),
|
||||
],
|
||||
'#default_value' => empty($media_embed_element['data-align']) ? 'none' : $media_embed_element['data-align'],
|
||||
'#attributes' => ['class' => ['container-inline']],
|
||||
'#parents' => ['attributes', 'data-align'],
|
||||
'#access' => $filter_align->status && ($filter_html->status === FALSE || !empty($allowed_attributes['data-align'])),
|
||||
];
|
||||
|
||||
// When Drupal core's filter_caption is being used, the text editor offers
|
||||
// the ability to in-place edit the media's caption: show a toggle.
|
||||
$form['caption'] = [
|
||||
'#title' => $this->t('Caption'),
|
||||
'#type' => 'checkbox',
|
||||
'#default_value' => $has_caption === 'true',
|
||||
'#parents' => ['hasCaption'],
|
||||
'#access' => $filter_caption->status && ($filter_html->status === FALSE || !empty($allowed_attributes['data-caption'])),
|
||||
];
|
||||
|
||||
$view_mode_options = array_intersect_key($this->entityDisplayRepository->getViewModeOptions('media'), $media_embed_filter->settings['allowed_view_modes']);
|
||||
$default_view_mode = static::getViewModeDefaultValue($view_mode_options, $media_embed_filter, $media_embed_element['data-view-mode'] ?? NULL);
|
||||
|
||||
$form['view_mode'] = [
|
||||
'#title' => $this->t("Display"),
|
||||
'#type' => 'select',
|
||||
'#options' => $view_mode_options,
|
||||
'#default_value' => $default_view_mode,
|
||||
'#parents' => ['attributes', 'data-view-mode'],
|
||||
'#access' => count($view_mode_options) >= 2,
|
||||
];
|
||||
|
||||
// Store the default from the MediaEmbed filter, so that if the selected
|
||||
// view mode matches the default, we can drop the 'data-view-mode'
|
||||
// attribute.
|
||||
$form_state->set('filter_default_view_mode', $media_embed_filter->settings['default_view_mode']);
|
||||
|
||||
if ((empty($form['alt']) || $form['alt']['#access'] === FALSE) && $form['align']['#access'] === FALSE && $form['caption']['#access'] === FALSE && $form['view_mode']['#access'] === FALSE) {
|
||||
$format = $editor->getFilterFormat();
|
||||
$warning = $this->t('There is nothing to configure for this media.');
|
||||
$form['no_access_notice'] = ['#markup' => $warning];
|
||||
if ($format->access('update')) {
|
||||
$tparams = [
|
||||
'@warning' => $warning,
|
||||
'@edit_url' => $format->toUrl('edit-form')->toString(),
|
||||
'%format' => $format->label(),
|
||||
];
|
||||
$form['no_access_notice']['#markup'] = $this->t('@warning <a href="@edit_url">Edit the text format %format</a> to modify the attributes that can be overridden.', $tparams);
|
||||
}
|
||||
}
|
||||
|
||||
$form['actions'] = [
|
||||
'#type' => 'actions',
|
||||
];
|
||||
$form['actions']['save_modal'] = [
|
||||
'#type' => 'submit',
|
||||
'#value' => $this->t('Save'),
|
||||
// No regular submit-handler. This form only works via JavaScript.
|
||||
'#submit' => [],
|
||||
'#ajax' => [
|
||||
'callback' => '::submitForm',
|
||||
'event' => 'click',
|
||||
],
|
||||
// Prevent this hidden element from being tabbable.
|
||||
'#attributes' => [
|
||||
'tabindex' => -1,
|
||||
],
|
||||
];
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function submitForm(array &$form, FormStateInterface $form_state) {
|
||||
$response = new AjaxResponse();
|
||||
|
||||
// When the `alt` attribute is set to two double quotes, transform it to the
|
||||
// empty string: two double quotes signify "empty alt attribute". See above.
|
||||
if (trim($form_state->getValue(['attributes', 'alt'], '')) === '""') {
|
||||
$form_state->setValue(['attributes', 'alt'], '""');
|
||||
}
|
||||
|
||||
// The `alt` attribute is optional: if it isn't set, the default value
|
||||
// simply will not be overridden. It's important to set it to FALSE
|
||||
// instead of unsetting the value. This way we explicitly inform
|
||||
// the client side about the new value.
|
||||
if ($form_state->hasValue(['attributes', 'alt']) && trim($form_state->getValue(['attributes', 'alt'])) === '') {
|
||||
$form_state->setValue(['attributes', 'alt'], FALSE);
|
||||
}
|
||||
|
||||
// If the selected view mode matches the default on the filter, remove the
|
||||
// attribute.
|
||||
if (!empty($form_state->get('filter_default_view_mode')) && $form_state->getValue(['attributes', 'data-view-mode']) === $form_state->get('filter_default_view_mode')) {
|
||||
$form_state->setValue(['attributes', 'data-view-mode'], FALSE);
|
||||
}
|
||||
|
||||
if ($form_state->getErrors()) {
|
||||
unset($form['#prefix'], $form['#suffix']);
|
||||
$form['status_messages'] = [
|
||||
'#type' => 'status_messages',
|
||||
'#weight' => -10,
|
||||
];
|
||||
$response->addCommand(new HtmlCommand('#editor-media-dialog-form', $form));
|
||||
}
|
||||
else {
|
||||
// Only send back the relevant values.
|
||||
$values = [
|
||||
'hasCaption' => $form_state->getValue('hasCaption'),
|
||||
'attributes' => $form_state->getValue('attributes'),
|
||||
];
|
||||
$response->addCommand(new EditorDialogSave($values));
|
||||
$response->addCommand(new CloseModalDialogCommand());
|
||||
}
|
||||
|
||||
return $response;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the default value for the view mode form element.
|
||||
*
|
||||
* @param array $view_mode_options
|
||||
* The array of options for the view mode form element.
|
||||
* @param \Drupal\filter\Plugin\FilterInterface $media_embed_filter
|
||||
* The media embed filter.
|
||||
* @param string $media_element_view_mode_attribute
|
||||
* The data-view-mode attribute on the <drupal-media> element.
|
||||
*
|
||||
* @return string|null
|
||||
* The default value for the view mode form element.
|
||||
*/
|
||||
public static function getViewModeDefaultValue(array $view_mode_options, FilterInterface $media_embed_filter, $media_element_view_mode_attribute) {
|
||||
// The select element won't display without at least two options, so if
|
||||
// that's the case, just return NULL.
|
||||
if (count($view_mode_options) < 2) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
$filter_default_view_mode = $media_embed_filter->settings['default_view_mode'];
|
||||
|
||||
// If the current media embed ($media_embed_element) has a set view mode,
|
||||
// we want to use that as the default in the select form element,
|
||||
// otherwise we'll want to use the default for all embedded media.
|
||||
if (!empty($media_element_view_mode_attribute) && array_key_exists($media_element_view_mode_attribute, $view_mode_options)) {
|
||||
return $media_element_view_mode_attribute;
|
||||
}
|
||||
elseif (array_key_exists($filter_default_view_mode, $view_mode_options)) {
|
||||
return $filter_default_view_mode;
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the name of an image media item's source field.
|
||||
*
|
||||
* @param \Drupal\media\MediaInterface $media
|
||||
* The media item being embedded.
|
||||
*
|
||||
* @return string|null
|
||||
* The name of the image source field configured for the media item, or
|
||||
* NULL if the source field is not an image field.
|
||||
*/
|
||||
protected function getMediaImageSourceFieldName(MediaInterface $media) {
|
||||
$field_definition = $media->getSource()
|
||||
->getSourceFieldDefinition($media->bundle->entity);
|
||||
$item_class = $field_definition->getItemDefinition()->getClass();
|
||||
if (is_a($item_class, ImageItem::class, TRUE)) {
|
||||
return $field_definition->getName();
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media\Form;
|
||||
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Form\ConfirmFormBase;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\Core\TempStore\PrivateTempStoreFactory;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\HttpFoundation\RedirectResponse;
|
||||
|
||||
/**
|
||||
* Provides a confirmation form to delete multiple media items at once.
|
||||
*
|
||||
* @deprecated in drupal:8.6.0 and is removed from drupal:9.0.0.
|
||||
* This route is not used in Drupal core. As an internal API, it may also be
|
||||
* removed in a minor release. If you are using it, copy the class
|
||||
* and the related "entity.media.multiple_delete_confirm" route to your
|
||||
* module.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class MediaDeleteMultipleConfirmForm extends ConfirmFormBase {
|
||||
|
||||
/**
|
||||
* The array of media items to delete, indexed by ID and language.
|
||||
*
|
||||
* @var string[][]
|
||||
*/
|
||||
protected $mediaItems = [];
|
||||
|
||||
/**
|
||||
* The tempstore factory.
|
||||
*
|
||||
* @var \Drupal\Core\TempStore\PrivateTempStoreFactory
|
||||
*/
|
||||
protected $tempStoreFactory;
|
||||
|
||||
/**
|
||||
* The entity storage.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityStorageInterface
|
||||
*/
|
||||
protected $storage;
|
||||
|
||||
/**
|
||||
* Constructs a MediaDeleteMultipleConfirmForm form object.
|
||||
*
|
||||
* @param \Drupal\Core\TempStore\PrivateTempStoreFactory $temp_store_factory
|
||||
* The tempstore factory.
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $manager
|
||||
* The entity type manager.
|
||||
*/
|
||||
public function __construct(PrivateTempStoreFactory $temp_store_factory, EntityTypeManagerInterface $manager) {
|
||||
@trigger_error(__CLASS__ . ' is deprecated in Drupal 8.6.0 and will be removed before Drupal 9.0.0. It is not used in Drupal core. As an internal API, it may also be removed in a minor release. If you are using it, copy the class and the related "entity.media.multiple_delete_confirm" route to your module.', E_USER_DEPRECATED);
|
||||
$this->tempStoreFactory = $temp_store_factory;
|
||||
$this->storage = $manager->getStorage('media');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('tempstore.private'),
|
||||
$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) - <em>The following translations will be deleted:</em>', ['@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) {
|
||||
$this->messenger()->addStatus($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');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media\Form;
|
||||
|
||||
use Drupal\Core\Config\ConfigFactoryInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Form\ConfigFormBase;
|
||||
use Drupal\media\IFrameUrlHelper;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Provides a form to configure Media settings.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class MediaSettingsForm extends ConfigFormBase {
|
||||
|
||||
/**
|
||||
* The iFrame URL helper service.
|
||||
*
|
||||
* @var \Drupal\media\IFrameUrlHelper
|
||||
*/
|
||||
protected $iFrameUrlHelper;
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* MediaSettingsForm constructor.
|
||||
*
|
||||
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
|
||||
* The config factory service.
|
||||
* @param \Drupal\media\IFrameUrlHelper $iframe_url_helper
|
||||
* The iFrame URL helper service.
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
*/
|
||||
public function __construct(ConfigFactoryInterface $config_factory, IFrameUrlHelper $iframe_url_helper, EntityTypeManagerInterface $entity_type_manager) {
|
||||
parent::__construct($config_factory);
|
||||
$this->iFrameUrlHelper = $iframe_url_helper;
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('config.factory'),
|
||||
$container->get('media.oembed.iframe_url_helper'),
|
||||
$container->get('entity_type.manager')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFormId() {
|
||||
return 'media_settings_form';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getEditableConfigNames() {
|
||||
return ['media.settings'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildForm(array $form, FormStateInterface $form_state) {
|
||||
$domain = $this->config('media.settings')->get('iframe_domain');
|
||||
|
||||
if (!$this->iFrameUrlHelper->isSecure($domain)) {
|
||||
$message = $this->t('It is potentially insecure to display oEmbed content in a frame that is served from the same domain as your main Drupal site, as this may allow execution of third-party code. <a href="https://oembed.com/#section3" target="_blank">Take a look here for more information</a>.');
|
||||
$this->messenger()->addWarning($message);
|
||||
}
|
||||
|
||||
$description = '<p>' . $this->t('Displaying media assets from third-party services, such as YouTube or Twitter, can be risky. This is because many of these services return arbitrary HTML to represent those assets, and that HTML may contain executable JavaScript code. If handled improperly, this can increase the risk of your site being compromised.') . '</p>';
|
||||
$description .= '<p>' . $this->t('In order to mitigate the risks, third-party assets are displayed in an iFrame, which effectively sandboxes any executable code running inside it. For even more security, the iFrame can be served from an alternate domain (that also points to your Drupal site), which you can configure on this page. This helps safeguard cookies and other sensitive information.') . '</p>';
|
||||
|
||||
$form['security'] = [
|
||||
'#type' => 'details',
|
||||
'#title' => $this->t('Security'),
|
||||
'#description' => $description,
|
||||
'#open' => TRUE,
|
||||
];
|
||||
// @todo Figure out how and if we should validate that this domain actually
|
||||
// points back to Drupal.
|
||||
// See https://www.drupal.org/project/drupal/issues/2965979 for more info.
|
||||
$form['security']['iframe_domain'] = [
|
||||
'#type' => 'url',
|
||||
'#title' => $this->t('iFrame domain'),
|
||||
'#size' => 40,
|
||||
'#maxlength' => 255,
|
||||
'#default_value' => $domain,
|
||||
'#description' => $this->t('Enter a different domain from which to serve oEmbed content, including the <em>http://</em> or <em>https://</em> prefix. This domain needs to point back to this site, or existing oEmbed content may not display correctly, or at all.'),
|
||||
];
|
||||
|
||||
$form['security']['standalone_url'] = [
|
||||
'#prefix' => '<hr>',
|
||||
'#type' => 'checkbox',
|
||||
'#title' => $this->t('Standalone media URL'),
|
||||
'#default_value' => $this->config('media.settings')->get('standalone_url'),
|
||||
'#description' => $this->t("Allow users to access @media-entities at /media/{id}.", ['@media-entities' => $this->entityTypeManager->getDefinition('media')->getPluralLabel()]),
|
||||
];
|
||||
return parent::buildForm($form, $form_state);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function submitForm(array &$form, FormStateInterface $form_state) {
|
||||
$this->config('media.settings')
|
||||
->set('iframe_domain', $form_state->getValue('iframe_domain'))
|
||||
->set('standalone_url', $form_state->getValue('standalone_url'))
|
||||
->save();
|
||||
|
||||
parent::submitForm($form, $form_state);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media\Form;
|
||||
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Entity\EntityDeleteForm;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Provides a form for media type deletion.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class MediaTypeDeleteConfirmForm extends EntityDeleteForm {
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* Constructs a new MediaTypeDeleteConfirm object.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
*/
|
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
|
||||
$this->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' => '<p>{{ message }}</p>',
|
||||
'#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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media;
|
||||
|
||||
use Drupal\Component\Render\MarkupInterface;
|
||||
use Drupal\Component\Render\MarkupTrait;
|
||||
|
||||
/**
|
||||
* Defines an object that wraps oEmbed markup for use in an iFrame.
|
||||
*
|
||||
* This object is not constructed with a known safe string as the strings come
|
||||
* from an external site. It must not be used outside the Media module's oEmbed
|
||||
* iframe rendering.
|
||||
*
|
||||
* @internal
|
||||
* This object is an internal part of the oEmbed system and should only be
|
||||
* used in \Drupal\media\Controller\OEmbedIframeController.
|
||||
*
|
||||
* @see \Drupal\media\Controller\OEmbedIframeController
|
||||
*/
|
||||
class IFrameMarkup implements MarkupInterface {
|
||||
use MarkupTrait;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media;
|
||||
|
||||
use Drupal\Component\Utility\Crypt;
|
||||
use Drupal\Core\PrivateKey;
|
||||
use Drupal\Core\Routing\RequestContext;
|
||||
use Drupal\Core\Site\Settings;
|
||||
|
||||
/**
|
||||
* Providers helper functions for displaying oEmbed resources in an iFrame.
|
||||
*
|
||||
* @internal
|
||||
* This is an internal part of the oEmbed system and should only be used by
|
||||
* oEmbed-related code in Drupal core.
|
||||
*/
|
||||
class IFrameUrlHelper {
|
||||
|
||||
/**
|
||||
* The request context service.
|
||||
*
|
||||
* @var \Drupal\Core\Routing\RequestContext
|
||||
*/
|
||||
protected $requestContext;
|
||||
|
||||
/**
|
||||
* The private key service.
|
||||
*
|
||||
* @var \Drupal\Core\PrivateKey
|
||||
*/
|
||||
protected $privateKey;
|
||||
|
||||
/**
|
||||
* IFrameUrlHelper constructor.
|
||||
*
|
||||
* @param \Drupal\Core\Routing\RequestContext $request_context
|
||||
* The request context service.
|
||||
* @param \Drupal\Core\PrivateKey $private_key
|
||||
* The private key service.
|
||||
*/
|
||||
public function __construct(RequestContext $request_context, PrivateKey $private_key) {
|
||||
$this->requestContext = $request_context;
|
||||
$this->privateKey = $private_key;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hashes an oEmbed resource URL.
|
||||
*
|
||||
* @param string $url
|
||||
* The resource URL.
|
||||
* @param int $max_width
|
||||
* (optional) The maximum width of the resource.
|
||||
* @param int $max_height
|
||||
* (optional) The maximum height of the resource.
|
||||
*
|
||||
* @return string
|
||||
* The hashed URL.
|
||||
*/
|
||||
public function getHash($url, $max_width = NULL, $max_height = NULL) {
|
||||
return Crypt::hmacBase64("$url:$max_width:$max_height", $this->privateKey->get() . Settings::getHashSalt());
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if an oEmbed URL can be securely displayed in an frame.
|
||||
*
|
||||
* @param string $url
|
||||
* The URL to check.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the URL is considered secure, otherwise FALSE.
|
||||
*/
|
||||
public function isSecure($url) {
|
||||
if (!$url) {
|
||||
return FALSE;
|
||||
}
|
||||
$url_host = parse_url($url, PHP_URL_HOST);
|
||||
$system_host = parse_url($this->requestContext->getCompleteBaseUrl(), PHP_URL_HOST);
|
||||
|
||||
// The URL is secure if its domain is not the same as the domain of the base
|
||||
// URL of the current request.
|
||||
return $url_host && $system_host && $url_host !== $system_host;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media;
|
||||
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\Core\Entity\EntityAccessControlHandler;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
|
||||
/**
|
||||
* Defines an access control handler for media items.
|
||||
*/
|
||||
class MediaAccessControlHandler extends EntityAccessControlHandler {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
|
||||
if ($account->hasPermission('administer media')) {
|
||||
return AccessResult::allowed()->cachePerPermissions();
|
||||
}
|
||||
|
||||
$type = $entity->bundle();
|
||||
$is_owner = ($account->id() && $account->id() === $entity->getOwnerId());
|
||||
switch ($operation) {
|
||||
case 'view':
|
||||
if ($entity->isPublished()) {
|
||||
$access_result = AccessResult::allowedIf($account->hasPermission('view media'))
|
||||
->cachePerPermissions()
|
||||
->addCacheableDependency($entity);
|
||||
if (!$access_result->isAllowed()) {
|
||||
$access_result->setReason("The 'view media' permission is required when the media item is published.");
|
||||
}
|
||||
}
|
||||
elseif ($account->hasPermission('view own unpublished media')) {
|
||||
$access_result = AccessResult::allowedIf($is_owner)
|
||||
->cachePerPermissions()
|
||||
->cachePerUser()
|
||||
->addCacheableDependency($entity);
|
||||
if (!$access_result->isAllowed()) {
|
||||
$access_result->setReason("The user must be the owner and the 'view own unpublished media' permission is required when the media item is unpublished.");
|
||||
}
|
||||
}
|
||||
else {
|
||||
$access_result = AccessResult::neutral()
|
||||
->cachePerPermissions()
|
||||
->addCacheableDependency($entity)
|
||||
->setReason("The user must be the owner and the 'view own unpublished media' permission is required when the media item is unpublished.");
|
||||
}
|
||||
return $access_result;
|
||||
|
||||
case 'update':
|
||||
if ($account->hasPermission('edit any ' . $type . ' media')) {
|
||||
return AccessResult::allowed()->cachePerPermissions();
|
||||
}
|
||||
if ($account->hasPermission('edit own ' . $type . ' media') && $is_owner) {
|
||||
return AccessResult::allowed()->cachePerPermissions()->cachePerUser()->addCacheableDependency($entity);
|
||||
}
|
||||
// @todo Deprecate this permission in
|
||||
// https://www.drupal.org/project/drupal/issues/2925459.
|
||||
if ($account->hasPermission('update any media')) {
|
||||
return AccessResult::allowed()->cachePerPermissions();
|
||||
}
|
||||
if ($account->hasPermission('update media') && $is_owner) {
|
||||
return AccessResult::allowed()->cachePerPermissions()->cachePerUser()->addCacheableDependency($entity);
|
||||
}
|
||||
return AccessResult::neutral("The following permissions are required: 'update any media' OR 'update own media' OR '$type: edit any media' OR '$type: edit own media'.")->cachePerPermissions();
|
||||
|
||||
case 'delete':
|
||||
if ($account->hasPermission('delete any ' . $type . ' media')) {
|
||||
return AccessResult::allowed()->cachePerPermissions();
|
||||
}
|
||||
if ($account->hasPermission('delete own ' . $type . ' media') && $is_owner) {
|
||||
return AccessResult::allowed()->cachePerPermissions()->cachePerUser()->addCacheableDependency($entity);
|
||||
}
|
||||
// @todo Deprecate this permission in
|
||||
// https://www.drupal.org/project/drupal/issues/2925459.
|
||||
if ($account->hasPermission('delete any media')) {
|
||||
return AccessResult::allowed()->cachePerPermissions();
|
||||
}
|
||||
if ($account->hasPermission('delete media') && $is_owner) {
|
||||
return AccessResult::allowed()->cachePerPermissions()->cachePerUser()->addCacheableDependency($entity);
|
||||
}
|
||||
return AccessResult::neutral("The following permissions are required: 'delete any media' OR 'delete own media' OR '$type: delete any media' OR '$type: delete own media'.")->cachePerPermissions();
|
||||
|
||||
default:
|
||||
return AccessResult::neutral()->cachePerPermissions();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function checkCreateAccess(AccountInterface $account, array $context, $entity_bundle = NULL) {
|
||||
$permissions = [
|
||||
'administer media',
|
||||
'create media',
|
||||
'create ' . $entity_bundle . ' media',
|
||||
];
|
||||
return AccessResult::allowedIfHasPermissions($account, $permissions, 'OR');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,88 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media;
|
||||
|
||||
use Drupal\Core\Entity\ContentEntityForm;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
|
||||
/**
|
||||
* Form controller for the media edit forms.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class MediaForm extends ContentEntityForm {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function form(array $form, FormStateInterface $form_state) {
|
||||
$form = parent::form($form, $form_state);
|
||||
/** @var \Drupal\media\MediaTypeInterface $media_type */
|
||||
$media_type = $this->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/form';
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function save(array $form, FormStateInterface $form_state) {
|
||||
$saved = parent::save($form, $form_state);
|
||||
$context = ['@type' => $this->entity->bundle(), '%label' => $this->entity->label(), 'link' => $this->entity->toLink($this->t('View'))->toString()];
|
||||
$logger = $this->logger('media');
|
||||
$t_args = ['@type' => $this->entity->bundle->entity->label(), '%label' => $this->entity->toLink($this->entity->label())->toString()];
|
||||
|
||||
if ($saved === SAVED_NEW) {
|
||||
$logger->notice('@type: added %label.', $context);
|
||||
$this->messenger()->addStatus($this->t('@type %label has been created.', $t_args));
|
||||
}
|
||||
else {
|
||||
$logger->notice('@type: updated %label.', $context);
|
||||
$this->messenger()->addStatus($this->t('@type %label has been updated.', $t_args));
|
||||
}
|
||||
|
||||
// Redirect the user to the media overview if the user has the 'access media
|
||||
// overview' permission. If not, redirect to the canonical URL of the media
|
||||
// item.
|
||||
if ($this->currentUser()->hasPermission('access media overview')) {
|
||||
$form_state->setRedirectUrl($this->entity->toUrl('collection'));
|
||||
}
|
||||
else {
|
||||
$form_state->setRedirectUrl($this->entity->toUrl());
|
||||
}
|
||||
|
||||
return $saved;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media;
|
||||
|
||||
use Drupal\Core\Entity\EntityChangedInterface;
|
||||
use Drupal\Core\Entity\ContentEntityInterface;
|
||||
use Drupal\Core\Entity\EntityPublishedInterface;
|
||||
use Drupal\Core\Entity\RevisionLogInterface;
|
||||
use Drupal\user\EntityOwnerInterface;
|
||||
|
||||
/**
|
||||
* Provides an interface defining an entity for media items.
|
||||
*/
|
||||
interface MediaInterface extends ContentEntityInterface, EntityChangedInterface, RevisionLogInterface, EntityOwnerInterface, EntityPublishedInterface {
|
||||
|
||||
/**
|
||||
* Gets the media item name.
|
||||
*
|
||||
* @return string
|
||||
* The name of the media item.
|
||||
*/
|
||||
public function getName();
|
||||
|
||||
/**
|
||||
* Sets the media item name.
|
||||
*
|
||||
* @param string $name
|
||||
* The name of the media item.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setName($name);
|
||||
|
||||
/**
|
||||
* Returns the media item creation timestamp.
|
||||
*
|
||||
* @todo Remove and use the new interface when #2833378 is done.
|
||||
* @see https://www.drupal.org/node/2833378
|
||||
*
|
||||
* @return int
|
||||
* Creation timestamp of the media item.
|
||||
*/
|
||||
public function getCreatedTime();
|
||||
|
||||
/**
|
||||
* Sets the media item creation timestamp.
|
||||
*
|
||||
* @todo Remove and use the new interface when #2833378 is done.
|
||||
* @see https://www.drupal.org/node/2833378
|
||||
*
|
||||
* @param int $timestamp
|
||||
* The media creation timestamp.
|
||||
*
|
||||
* @return $this
|
||||
* The called media item.
|
||||
*/
|
||||
public function setCreatedTime($timestamp);
|
||||
|
||||
/**
|
||||
* Returns the media source.
|
||||
*
|
||||
* @return \Drupal\media\MediaSourceInterface
|
||||
* The media source.
|
||||
*/
|
||||
public function getSource();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media;
|
||||
|
||||
use Drupal\Core\Datetime\DateFormatterInterface;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\EntityListBuilder;
|
||||
use Drupal\Core\Entity\EntityStorageInterface;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Language\LanguageManagerInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Provides a listing of media items.
|
||||
*/
|
||||
class MediaListBuilder extends EntityListBuilder {
|
||||
|
||||
/**
|
||||
* The date formatter service.
|
||||
*
|
||||
* @var \Drupal\Core\Datetime\DateFormatterInterface
|
||||
*/
|
||||
protected $dateFormatter;
|
||||
|
||||
/**
|
||||
* The language manager service.
|
||||
*
|
||||
* @var \Drupal\Core\Language\LanguageManagerInterface
|
||||
*/
|
||||
protected $languageManager;
|
||||
|
||||
/**
|
||||
* Indicates whether the 'thumbnail' image style exists.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $thumbnailStyleExists = FALSE;
|
||||
|
||||
/**
|
||||
* Constructs a new MediaListBuilder object.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeInterface $entity_type
|
||||
* The entity type definition.
|
||||
* @param \Drupal\Core\Entity\EntityStorageInterface $storage
|
||||
* The entity storage class.
|
||||
* @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
|
||||
* The date formatter service.
|
||||
* @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
|
||||
* The language manager service.
|
||||
* @param \Drupal\Core\Entity\EntityStorageInterface $image_style_storage
|
||||
* The entity storage class for image styles.
|
||||
*/
|
||||
public function __construct(EntityTypeInterface $entity_type, EntityStorageInterface $storage, DateFormatterInterface $date_formatter, LanguageManagerInterface $language_manager, EntityStorageInterface $image_style_storage) {
|
||||
parent::__construct($entity_type, $storage);
|
||||
|
||||
$this->dateFormatter = $date_formatter;
|
||||
$this->languageManager = $language_manager;
|
||||
$this->thumbnailStyleExists = !empty($image_style_storage->load('thumbnail'));
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
|
||||
$entity_type_manager = $container->get('entity_type.manager');
|
||||
return new static(
|
||||
$entity_type,
|
||||
$entity_type_manager->getStorage($entity_type->id()),
|
||||
$container->get('date.formatter'),
|
||||
$container->get('language_manager'),
|
||||
$entity_type_manager->getStorage('image_style')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildHeader() {
|
||||
$header = [];
|
||||
if ($this->thumbnailStyleExists) {
|
||||
$header['thumbnail'] = [
|
||||
'data' => $this->t('Thumbnail'),
|
||||
'class' => [RESPONSIVE_PRIORITY_LOW],
|
||||
];
|
||||
}
|
||||
$header += [
|
||||
'name' => $this->t('Media Name'),
|
||||
'type' => [
|
||||
'data' => $this->t('Type'),
|
||||
'class' => [RESPONSIVE_PRIORITY_MEDIUM],
|
||||
],
|
||||
'author' => [
|
||||
'data' => $this->t('Author'),
|
||||
'class' => [RESPONSIVE_PRIORITY_LOW],
|
||||
],
|
||||
'status' => $this->t('Status'),
|
||||
'changed' => [
|
||||
'data' => $this->t('Updated'),
|
||||
'class' => [RESPONSIVE_PRIORITY_LOW],
|
||||
],
|
||||
];
|
||||
// Enable language column if multiple languages are added.
|
||||
if ($this->languageManager->isMultilingual()) {
|
||||
$header['language'] = [
|
||||
'data' => $this->t('Language'),
|
||||
'class' => [RESPONSIVE_PRIORITY_LOW],
|
||||
];
|
||||
}
|
||||
return $header + parent::buildHeader();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildRow(EntityInterface $entity) {
|
||||
/** @var \Drupal\media\MediaInterface $entity */
|
||||
if ($this->thumbnailStyleExists) {
|
||||
$row['thumbnail'] = [];
|
||||
if ($thumbnail_uri = $entity->getSource()->getMetadata($entity, 'thumbnail_uri')) {
|
||||
$row['thumbnail']['data'] = [
|
||||
'#theme' => 'image_style',
|
||||
'#style_name' => 'thumbnail',
|
||||
'#uri' => $thumbnail_uri,
|
||||
'#height' => 50,
|
||||
];
|
||||
}
|
||||
}
|
||||
$row['name']['data'] = [
|
||||
'#type' => 'link',
|
||||
'#title' => $entity->label(),
|
||||
'#url' => $entity->toUrl(),
|
||||
];
|
||||
$row['type'] = $entity->bundle->entity->label();
|
||||
$row['author']['data'] = [
|
||||
'#theme' => 'username',
|
||||
'#account' => $entity->getOwner(),
|
||||
];
|
||||
$row['status'] = $entity->isPublished() ? $this->t('Published') : $this->t('Unpublished');
|
||||
$row['changed'] = $this->dateFormatter->format($entity->getChangedTime(), 'short');
|
||||
|
||||
if ($this->languageManager->isMultilingual()) {
|
||||
$row['language'] = $this->languageManager->getLanguageName($entity->language()->getId());
|
||||
}
|
||||
return $row + parent::buildRow($entity);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getEntityIds() {
|
||||
$query = $this->getStorage()->getQuery()
|
||||
->sort('changed', 'DESC');
|
||||
|
||||
// Only add the pager if a limit is specified.
|
||||
if ($this->limit) {
|
||||
$query->pager($this->limit);
|
||||
}
|
||||
return $query->execute();
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media;
|
||||
|
||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Provides dynamic permissions for each media type.
|
||||
*/
|
||||
class MediaPermissions implements ContainerInjectionInterface {
|
||||
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* The entity type manager service.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* MediaPermissions constructor.
|
||||
*
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager service.
|
||||
*/
|
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager) {
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static($container->get('entity_type.manager'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of media type permissions.
|
||||
*
|
||||
* @return array
|
||||
* The media type permissions.
|
||||
*
|
||||
* @see \Drupal\user\PermissionHandlerInterface::getPermissions()
|
||||
*/
|
||||
public function mediaTypePermissions() {
|
||||
$perms = [];
|
||||
// Generate media permissions for all media types.
|
||||
$media_types = $this->entityTypeManager
|
||||
->getStorage('media_type')->loadMultiple();
|
||||
foreach ($media_types as $type) {
|
||||
$perms += $this->buildPermissions($type);
|
||||
}
|
||||
return $perms;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of media permissions for a given media type.
|
||||
*
|
||||
* @param \Drupal\media\MediaTypeInterface $type
|
||||
* The media type.
|
||||
*
|
||||
* @return array
|
||||
* An associative array of permission names and descriptions.
|
||||
*/
|
||||
protected function buildPermissions(MediaTypeInterface $type) {
|
||||
$type_id = $type->id();
|
||||
$type_params = ['%type_name' => $type->label()];
|
||||
|
||||
return [
|
||||
"create $type_id media" => [
|
||||
'title' => $this->t('%type_name: Create new media', $type_params),
|
||||
],
|
||||
"edit own $type_id media" => [
|
||||
'title' => $this->t('%type_name: Edit own media', $type_params),
|
||||
],
|
||||
"edit any $type_id media" => [
|
||||
'title' => $this->t('%type_name: Edit any media', $type_params),
|
||||
],
|
||||
"delete own $type_id media" => [
|
||||
'title' => $this->t('%type_name: Delete own media', $type_params),
|
||||
],
|
||||
"delete any $type_id media" => [
|
||||
'title' => $this->t('%type_name: Delete any media', $type_params),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,363 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media;
|
||||
|
||||
use Drupal\Component\Utility\NestedArray;
|
||||
use Drupal\Core\Entity\Display\EntityFormDisplayInterface;
|
||||
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
|
||||
use Drupal\Core\Entity\EntityFieldManagerInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Field\FieldTypePluginManagerInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Config\ConfigFactoryInterface;
|
||||
use Drupal\Core\Plugin\PluginBase;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Base implementation of media source plugin.
|
||||
*/
|
||||
abstract class MediaSourceBase extends PluginBase implements MediaSourceInterface, ContainerFactoryPluginInterface {
|
||||
|
||||
/**
|
||||
* Plugin label.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $label;
|
||||
|
||||
/**
|
||||
* The entity type manager service.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The entity field manager service.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
|
||||
*/
|
||||
protected $entityFieldManager;
|
||||
|
||||
/**
|
||||
* The field type plugin manager service.
|
||||
*
|
||||
* @var \Drupal\Core\Field\FieldTypePluginManagerInterface
|
||||
*/
|
||||
protected $fieldTypeManager;
|
||||
|
||||
/**
|
||||
* The config factory service.
|
||||
*
|
||||
* @var \Drupal\Core\Config\ConfigFactoryInterface
|
||||
*/
|
||||
protected $configFactory;
|
||||
|
||||
/**
|
||||
* Constructs a new class instance.
|
||||
*
|
||||
* @param array $configuration
|
||||
* A configuration array containing information about the plugin instance.
|
||||
* @param string $plugin_id
|
||||
* The plugin_id for the plugin instance.
|
||||
* @param mixed $plugin_definition
|
||||
* The plugin implementation definition.
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* Entity type manager service.
|
||||
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
|
||||
* Entity field manager service.
|
||||
* @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager
|
||||
* The field type plugin manager service.
|
||||
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
|
||||
* The config factory service.
|
||||
*/
|
||||
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, FieldTypePluginManagerInterface $field_type_manager, ConfigFactoryInterface $config_factory) {
|
||||
parent::__construct($configuration, $plugin_id, $plugin_definition);
|
||||
$this->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']),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
// Some media sources are using a deriver, so their plugin IDs may contain
|
||||
// a separator (usually ':') which is not allowed in field names.
|
||||
$base_id = 'field_media_' . str_replace(static::DERIVATIVE_SEPARATOR, '_', $this->getPluginId());
|
||||
$tries = 0;
|
||||
$storage = $this->entityTypeManager->getStorage('field_storage_config');
|
||||
|
||||
// Iterate at least once, until no field with the generated ID is found.
|
||||
do {
|
||||
$id = $base_id;
|
||||
// If we've tried before, increment and append the suffix.
|
||||
if ($tries) {
|
||||
$id .= '_' . $tries;
|
||||
}
|
||||
$field = $storage->load('media.' . $id);
|
||||
$tries++;
|
||||
} while ($field);
|
||||
|
||||
return $id;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getSourceFieldValue(MediaInterface $media) {
|
||||
$source_field = $this->configuration['source_field'];
|
||||
if (empty($source_field)) {
|
||||
throw new \RuntimeException('Source field for media source is not defined.');
|
||||
}
|
||||
|
||||
$items = $media->get($source_field);
|
||||
if ($items->isEmpty()) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
$field_item = $items->first();
|
||||
return $field_item->{$field_item->mainPropertyName()};
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function prepareViewDisplay(MediaTypeInterface $type, EntityViewDisplayInterface $display) {
|
||||
$display->setComponent($this->getSourceFieldDefinition($type)->getName(), [
|
||||
'label' => 'visually_hidden',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function prepareFormDisplay(MediaTypeInterface $type, EntityFormDisplayInterface $display) {
|
||||
// Make sure the source field is placed just after the "name" basefield.
|
||||
$name_component = $display->getComponent('name');
|
||||
$source_field_weight = ($name_component && isset($name_component['weight'])) ? $name_component['weight'] + 5 : -50;
|
||||
$display->setComponent($this->getSourceFieldDefinition($type)->getName(), [
|
||||
'weight' => $source_field_weight,
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media;
|
||||
|
||||
/**
|
||||
* Defines an interface for a media source with entity constraints.
|
||||
*
|
||||
* This allows a media source to optionally add entity validation constraints
|
||||
* for media items. To add constraints at the source field level, a media source
|
||||
* can also implement MediaSourceFieldConstraintsInterface.
|
||||
*
|
||||
* @see \Drupal\media\MediaSourceInterface
|
||||
* @see \Drupal\media\MediaSourceFieldConstraintsInterface.php
|
||||
* @see \Drupal\media\MediaSourceBase
|
||||
* @see \Drupal\media\Entity\Media
|
||||
*/
|
||||
interface MediaSourceEntityConstraintsInterface extends MediaSourceInterface {
|
||||
|
||||
/**
|
||||
* Gets media source-specific validation constraints for a media item.
|
||||
*
|
||||
* @return \Symfony\Component\Validator\Constraint[]
|
||||
* An array of validation constraint definitions, keyed by constraint name.
|
||||
* Each constraint definition can be used for instantiating
|
||||
* \Symfony\Component\Validator\Constraint objects.
|
||||
*/
|
||||
public function getEntityConstraints();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media;
|
||||
|
||||
/**
|
||||
* Defines an interface for a media source with source field constraints.
|
||||
*
|
||||
* This allows a media source to optionally add source field validation
|
||||
* constraints for media items. To add constraints at the entity level, a
|
||||
* media source can also implement MediaSourceEntityConstraintsInterface.
|
||||
*
|
||||
* @see \Drupal\media\MediaSourceInterface
|
||||
* @see \Drupal\media\MediaSourceEntityConstraintsInterface
|
||||
* @see \Drupal\media\MediaSourceBase
|
||||
* @see \Drupal\media\Entity\Media
|
||||
*/
|
||||
interface MediaSourceFieldConstraintsInterface extends MediaSourceInterface {
|
||||
|
||||
/**
|
||||
* Gets media source-specific validation constraints for a source field.
|
||||
*
|
||||
* @return \Symfony\Component\Validator\Constraint[]
|
||||
* An array of validation constraint definitions, keyed by constraint name.
|
||||
* Each constraint definition can be used for instantiating
|
||||
* \Symfony\Component\Validator\Constraint objects.
|
||||
*/
|
||||
public function getSourceFieldConstraints();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,196 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media;
|
||||
|
||||
use Drupal\Component\Plugin\ConfigurableInterface;
|
||||
use Drupal\Component\Plugin\ConfigurablePluginInterface;
|
||||
use Drupal\Component\Plugin\DependentPluginInterface;
|
||||
use Drupal\Component\Plugin\PluginInspectionInterface;
|
||||
use Drupal\Core\Entity\Display\EntityFormDisplayInterface;
|
||||
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
|
||||
use Drupal\Core\Plugin\PluginFormInterface;
|
||||
|
||||
/**
|
||||
* Defines the interface for media source plugins.
|
||||
*
|
||||
* Media sources provide the critical link between media items in Drupal and the
|
||||
* actual media itself, which typically exists independently of Drupal. Each
|
||||
* media source works with a certain kind of media. For example, local files and
|
||||
* YouTube videos can both be catalogued in a similar way as media items, but
|
||||
* they need very different handling to actually display them.
|
||||
*
|
||||
* Each media type needs exactly one source. A single source can be used on many
|
||||
* media types.
|
||||
*
|
||||
* Examples of possible sources are:
|
||||
* - File: handles local files,
|
||||
* - Image: handles local images,
|
||||
* - oEmbed: handles resources that are exposed through the oEmbed standard,
|
||||
* - YouTube: handles YouTube videos,
|
||||
* - SoundCloud: handles SoundCloud audio,
|
||||
* - Instagram: handles Instagram posts,
|
||||
* - Twitter: handles tweets,
|
||||
* - ...
|
||||
*
|
||||
* Their responsibilities are:
|
||||
* - Defining how media is represented (stored). Media sources are not
|
||||
* responsible for actually storing the media. They only define how it is
|
||||
* represented on a media item (usually using some kind of a field).
|
||||
* - Providing thumbnails. Media sources that are responsible for remote
|
||||
* media will generally fetch the image from a third-party API and make
|
||||
* it available for the local usage. Media sources that represent local
|
||||
* media (such as images) will usually use some locally provided image.
|
||||
* Media sources should fall back to a pre-defined default thumbnail if
|
||||
* everything else fails.
|
||||
* - Validating a media item before it is saved. The entity constraint system
|
||||
* will be used to ensure the valid structure of the media item.
|
||||
* For example, media sources that represent remote media might check the
|
||||
* URL or other identifier, while sources that represent local files might
|
||||
* check the MIME type of the file.
|
||||
* - Providing a default name for a media item. This will save users from
|
||||
* manually entering the name when it can be reliably set automatically.
|
||||
* Media sources for local files will generally use the filename, while media
|
||||
* sources for remote resources might obtain a title attribute through a
|
||||
* third-party API. The name can always be overridden by the user.
|
||||
* - Providing metadata specific to the given media type. For example, remote
|
||||
* media sources generally get information available through a
|
||||
* third-party API and make it available to Drupal, while local media sources
|
||||
* can expose things such as EXIF or ID3.
|
||||
* - Mapping metadata to the media item. Metadata that a media source exposes
|
||||
* can automatically be mapped to the fields on the media item. Media
|
||||
* sources will be able to define how this is done.
|
||||
*
|
||||
* @see \Drupal\media\Annotation\MediaSource
|
||||
* @see \Drupal\media\MediaSourceBase
|
||||
* @see \Drupal\media\MediaSourceManager
|
||||
* @see \Drupal\media\MediaTypeInterface
|
||||
* @see \Drupal\media\MediaSourceEntityConstraintsInterface
|
||||
* @see \Drupal\media\MediaSourceFieldConstraintsInterface
|
||||
* @see plugin_api
|
||||
*/
|
||||
interface MediaSourceInterface extends PluginInspectionInterface, ConfigurableInterface, DependentPluginInterface, ConfigurablePluginInterface, PluginFormInterface {
|
||||
|
||||
/**
|
||||
* Default empty value for metadata fields.
|
||||
*/
|
||||
const METADATA_FIELD_EMPTY = '_none';
|
||||
|
||||
/**
|
||||
* Gets a list of metadata attributes provided by this plugin.
|
||||
*
|
||||
* Most media sources have associated metadata, describing attributes
|
||||
* such as:
|
||||
* - dimensions
|
||||
* - duration
|
||||
* - encoding
|
||||
* - date
|
||||
* - location
|
||||
* - permalink
|
||||
* - licensing information
|
||||
* - ...
|
||||
*
|
||||
* This method should list all metadata attributes that a media source MAY
|
||||
* offer. In other words: it is possible that a particular media item does
|
||||
* not contain a certain attribute. For example: an oEmbed media source can
|
||||
* contain both video and images. Images don't have a duration, but
|
||||
* videos do.
|
||||
*
|
||||
* (The term 'attributes' was chosen because it cannot be confused
|
||||
* with 'fields' and 'properties', both of which are concepts in Drupal's
|
||||
* Entity Field API.)
|
||||
*
|
||||
* @return array
|
||||
* Associative array with:
|
||||
* - keys: metadata attribute names
|
||||
* - values: human-readable labels for those attribute names
|
||||
*/
|
||||
public function getMetadataAttributes();
|
||||
|
||||
/**
|
||||
* Gets the value for a metadata attribute for a given media item.
|
||||
*
|
||||
* @param \Drupal\media\MediaInterface $media
|
||||
* A media item.
|
||||
* @param string $attribute_name
|
||||
* Name of the attribute to fetch.
|
||||
*
|
||||
* @return mixed|null
|
||||
* Metadata attribute value or NULL if unavailable.
|
||||
*/
|
||||
public function getMetadata(MediaInterface $media, $attribute_name);
|
||||
|
||||
/**
|
||||
* Get the source field definition for a media type.
|
||||
*
|
||||
* @param \Drupal\media\MediaTypeInterface $type
|
||||
* A media type.
|
||||
*
|
||||
* @return \Drupal\Core\Field\FieldDefinitionInterface|null
|
||||
* The source field definition, or NULL if it doesn't exist or has not been
|
||||
* configured yet.
|
||||
*/
|
||||
public function getSourceFieldDefinition(MediaTypeInterface $type);
|
||||
|
||||
/**
|
||||
* Creates the source field definition for a type.
|
||||
*
|
||||
* @param \Drupal\media\MediaTypeInterface $type
|
||||
* The media type.
|
||||
*
|
||||
* @return \Drupal\field\FieldConfigInterface
|
||||
* The unsaved field definition. The field storage definition, if new,
|
||||
* should also be unsaved.
|
||||
*/
|
||||
public function createSourceField(MediaTypeInterface $type);
|
||||
|
||||
/**
|
||||
* Prepares the media type fields for this source in the view display.
|
||||
*
|
||||
* This method should normally call
|
||||
* \Drupal\Core\Entity\Display\EntityDisplayInterface::setComponent() or
|
||||
* \Drupal\Core\Entity\Display\EntityDisplayInterface::removeComponent() to
|
||||
* configure the media type fields in the view display.
|
||||
*
|
||||
* @param \Drupal\media\MediaTypeInterface $type
|
||||
* The media type which is using this source.
|
||||
* @param \Drupal\Core\Entity\Display\EntityViewDisplayInterface $display
|
||||
* The display which should be prepared.
|
||||
*
|
||||
* @see \Drupal\Core\Entity\Display\EntityDisplayInterface::setComponent()
|
||||
* @see \Drupal\Core\Entity\Display\EntityDisplayInterface::removeComponent()
|
||||
*/
|
||||
public function prepareViewDisplay(MediaTypeInterface $type, EntityViewDisplayInterface $display);
|
||||
|
||||
/**
|
||||
* Prepares the media type fields for this source in the form display.
|
||||
*
|
||||
* This method should normally call
|
||||
* \Drupal\Core\Entity\Display\EntityDisplayInterface::setComponent() or
|
||||
* \Drupal\Core\Entity\Display\EntityDisplayInterface::removeComponent() to
|
||||
* configure the media type fields in the form display.
|
||||
*
|
||||
* @param \Drupal\media\MediaTypeInterface $type
|
||||
* The media type which is using this source.
|
||||
* @param \Drupal\Core\Entity\Display\EntityFormDisplayInterface $display
|
||||
* The display which should be prepared.
|
||||
*
|
||||
* @see \Drupal\Core\Entity\Display\EntityDisplayInterface::setComponent()
|
||||
* @see \Drupal\Core\Entity\Display\EntityDisplayInterface::removeComponent()
|
||||
*/
|
||||
public function prepareFormDisplay(MediaTypeInterface $type, EntityFormDisplayInterface $display);
|
||||
|
||||
/**
|
||||
* Get the primary value stored in the source field.
|
||||
*
|
||||
* @param MediaInterface $media
|
||||
* A media item.
|
||||
*
|
||||
* @return mixed
|
||||
* The source value, or NULL if the media item's source field is empty.
|
||||
*
|
||||
* @throws \RuntimeException
|
||||
* If the source field for the media source is not defined.
|
||||
*/
|
||||
public function getSourceFieldValue(MediaInterface $media);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media;
|
||||
|
||||
use Drupal\Core\Cache\CacheBackendInterface;
|
||||
use Drupal\Core\Extension\ModuleHandlerInterface;
|
||||
use Drupal\Core\Plugin\DefaultPluginManager;
|
||||
use Drupal\media\Annotation\MediaSource;
|
||||
|
||||
/**
|
||||
* Manages media source plugins.
|
||||
*/
|
||||
class MediaSourceManager extends DefaultPluginManager {
|
||||
|
||||
/**
|
||||
* Constructs a new MediaSourceManager.
|
||||
*
|
||||
* @param \Traversable $namespaces
|
||||
* An object that implements \Traversable which contains the root paths
|
||||
* keyed by the corresponding namespace to look for plugin implementations.
|
||||
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
|
||||
* Cache backend instance to use.
|
||||
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
|
||||
* The module handler.
|
||||
*/
|
||||
public function __construct(\Traversable $namespaces, CacheBackendInterface $cache_backend, ModuleHandlerInterface $module_handler) {
|
||||
parent::__construct('Plugin/media/Source', $namespaces, $module_handler, MediaSourceInterface::class, MediaSource::class);
|
||||
|
||||
$this->alterInfo('media_source_info');
|
||||
$this->setCacheBackend($cache_backend, 'media_source_plugins');
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media;
|
||||
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Entity\Sql\SqlContentEntityStorage;
|
||||
|
||||
/**
|
||||
* Defines the storage handler class for media.
|
||||
*
|
||||
* The default storage is overridden to handle metadata fetching outside of the
|
||||
* database transaction.
|
||||
*/
|
||||
class MediaStorage extends SqlContentEntityStorage {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function save(EntityInterface $media) {
|
||||
// For backwards compatibility, modules that override the Media entity
|
||||
// class, are not required to implement the prepareSave() method.
|
||||
// @todo For Drupal 8.7, consider throwing a deprecation notice if the
|
||||
// method doesn't exist. See
|
||||
// https://www.drupal.org/project/drupal/issues/2992426 for further
|
||||
// discussion.
|
||||
if (method_exists($media, 'prepareSave')) {
|
||||
$media->prepareSave();
|
||||
}
|
||||
return parent::save($media);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media;
|
||||
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\Core\Entity\EntityAccessControlHandler;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
|
||||
/**
|
||||
* Defines the access control handler for the "Media Type" entity type.
|
||||
*
|
||||
* @see \Drupal\media\Entity\MediaType
|
||||
*/
|
||||
class MediaTypeAccessControlHandler extends EntityAccessControlHandler {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected $viewLabelOperation = TRUE;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function checkAccess(EntityInterface $entity, $operation, AccountInterface $account) {
|
||||
if ($operation === 'view label') {
|
||||
return AccessResult::allowedIfHasPermission($account, 'view media');
|
||||
}
|
||||
else {
|
||||
return parent::checkAccess($entity, $operation, $account);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,397 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media;
|
||||
|
||||
use Drupal\Component\Plugin\PluginManagerInterface;
|
||||
use Drupal\Core\Ajax\AjaxResponse;
|
||||
use Drupal\Core\Ajax\ReplaceCommand;
|
||||
use Drupal\Core\Entity\EntityDisplayRepositoryInterface;
|
||||
use Drupal\Core\Entity\EntityFieldManagerInterface;
|
||||
use Drupal\Core\Entity\EntityForm;
|
||||
use Drupal\Core\Field\BaseFieldDefinition;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Form\SubformState;
|
||||
use Drupal\language\Entity\ContentLanguageSettings;
|
||||
use Drupal\media\Entity\MediaType;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Form controller for media type forms.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class MediaTypeForm extends EntityForm {
|
||||
|
||||
/**
|
||||
* Media source plugin manager.
|
||||
*
|
||||
* @var \Drupal\Component\Plugin\PluginManagerInterface
|
||||
*/
|
||||
protected $sourceManager;
|
||||
|
||||
/**
|
||||
* Entity field manager service.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityFieldManagerInterface
|
||||
*/
|
||||
protected $entityFieldManager;
|
||||
|
||||
/**
|
||||
* Entity display repository service.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface
|
||||
*/
|
||||
protected $entityDisplayRepository;
|
||||
|
||||
/**
|
||||
* Constructs a new class instance.
|
||||
*
|
||||
* @param \Drupal\Component\Plugin\PluginManagerInterface $source_manager
|
||||
* Media source plugin manager.
|
||||
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
|
||||
* Entity field manager service.
|
||||
* @param \Drupal\Core\Entity\EntityDisplayRepositoryInterface $entityDisplayRepository
|
||||
* Entity display repository service.
|
||||
*/
|
||||
public function __construct(PluginManagerInterface $source_manager, EntityFieldManagerInterface $entity_field_manager, EntityDisplayRepositoryInterface $entityDisplayRepository) {
|
||||
$this->sourceManager = $source_manager;
|
||||
$this->entityFieldManager = $entity_field_manager;
|
||||
$this->entityDisplayRepository = $entityDisplayRepository;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('plugin.manager.media.source'),
|
||||
$container->get('entity_field.manager'),
|
||||
$container->get('entity_display.repository')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 <em>Add new media</em> 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'],
|
||||
];
|
||||
|
||||
if (!$this->entity->isNew()) {
|
||||
$source_description = $this->t('<em>The media source cannot be changed after the media type is created.</em>');
|
||||
}
|
||||
else {
|
||||
$source_description = $this->t('Media source that is responsible for additional logic related to this media type.');
|
||||
}
|
||||
$form['source_dependent']['source'] = [
|
||||
'#type' => 'select',
|
||||
'#title' => $this->t('Media source'),
|
||||
'#default_value' => $source ? $source->getPluginId() : NULL,
|
||||
'#options' => $options,
|
||||
'#description' => $source_description,
|
||||
'#ajax' => ['callback' => '::ajaxHandlerData'],
|
||||
'#required' => TRUE,
|
||||
// Once the media type is created, its source plugin cannot be changed
|
||||
// anymore.
|
||||
'#disabled' => !$this->entity->isNew(),
|
||||
];
|
||||
|
||||
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' => '<p>' . $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.') . '</p>',
|
||||
],
|
||||
];
|
||||
|
||||
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/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 (isset($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 (isset($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);
|
||||
|
||||
// If the media source has not been chosen yet, turn the submit button into
|
||||
// a button. This rebuilds the form with the media source's configuration
|
||||
// form visible, instead of saving the media type. This allows users to
|
||||
// create a media type without JavaScript enabled. With JavaScript enabled,
|
||||
// this rebuild occurs during an AJAX request.
|
||||
// @see \Drupal\media\MediaTypeForm::ajaxHandlerData()
|
||||
if (empty($this->getEntity()->get('source'))) {
|
||||
$actions['submit']['#type'] = 'button';
|
||||
}
|
||||
|
||||
$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.
|
||||
if ($source_field->isDisplayConfigurable('form')) {
|
||||
$display = $this->entityDisplayRepository->getFormDisplay('media', $media_type->id());
|
||||
$source->prepareFormDisplay($media_type, $display);
|
||||
$display->save();
|
||||
}
|
||||
if ($source_field->isDisplayConfigurable('view')) {
|
||||
$display = $this->entityDisplayRepository->getViewDisplay('media', $media_type->id());
|
||||
|
||||
// Remove all default components.
|
||||
foreach (array_keys($display->getComponents()) as $name) {
|
||||
$display->removeComponent($name);
|
||||
}
|
||||
$source->prepareViewDisplay($media_type, $display);
|
||||
$display->save();
|
||||
}
|
||||
}
|
||||
|
||||
$t_args = ['%name' => $media_type->label()];
|
||||
if ($status === SAVED_UPDATED) {
|
||||
$this->messenger()->addStatus($this->t('The media type %name has been updated.', $t_args));
|
||||
}
|
||||
elseif ($status === SAVED_NEW) {
|
||||
$this->messenger()->addStatus($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'));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,101 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media;
|
||||
|
||||
use Drupal\Core\Config\Entity\ConfigEntityInterface;
|
||||
use Drupal\Core\Entity\EntityDescriptionInterface;
|
||||
use Drupal\Core\Entity\RevisionableEntityBundleInterface;
|
||||
|
||||
/**
|
||||
* Provides an interface defining a media type entity.
|
||||
*
|
||||
* Media types are bundles for media items. They are used to group media with
|
||||
* the same semantics. Media types are not about where media comes from. They
|
||||
* are about the semantics that media has in the context of a given Drupal site.
|
||||
*
|
||||
* Media sources, on the other hand, are aware where media comes from and know
|
||||
* how to represent and handle it in Drupal's context. They are aware of the low
|
||||
* level details, while the media types don't care about them at all. That said,
|
||||
* media types can not exist without media sources.
|
||||
*
|
||||
* Consider the following examples:
|
||||
* - oEmbed media source which can represent any oEmbed resource. Media types
|
||||
* that could be used with this source are "Videos", "Charts", "Music", etc.
|
||||
* All of them are retrieved using the same protocol, but they represent very
|
||||
* different things.
|
||||
* - Media sources that represent files could be used with media types like
|
||||
* "Invoices", "Subtitles", "Meeting notes", etc. They are all files stored on
|
||||
* some kind of storage, but their meaning and uses in a Drupal site are
|
||||
* different.
|
||||
*
|
||||
* @see \Drupal\media\MediaSourceInterface
|
||||
*/
|
||||
interface MediaTypeInterface extends ConfigEntityInterface, EntityDescriptionInterface, RevisionableEntityBundleInterface {
|
||||
|
||||
/**
|
||||
* Returns whether thumbnail downloads are queued.
|
||||
*
|
||||
* 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.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if thumbnails are queued for download later, FALSE if they should be
|
||||
* downloaded now.
|
||||
*/
|
||||
public function thumbnailDownloadsAreQueued();
|
||||
|
||||
/**
|
||||
* Sets a flag to indicate that thumbnails should be downloaded via a queue.
|
||||
*
|
||||
* @param bool $queue_thumbnail_downloads
|
||||
* The queue downloads flag.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setQueueThumbnailDownloadsStatus($queue_thumbnail_downloads);
|
||||
|
||||
/**
|
||||
* Returns the media source plugin.
|
||||
*
|
||||
* @return \Drupal\media\MediaSourceInterface
|
||||
* The media source.
|
||||
*/
|
||||
public function getSource();
|
||||
|
||||
/**
|
||||
* Sets whether new revisions should be created by default.
|
||||
*
|
||||
* @param bool $new_revision
|
||||
* TRUE if media items of this type should create new revisions by default.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setNewRevision($new_revision);
|
||||
|
||||
/**
|
||||
* Returns the metadata field map.
|
||||
*
|
||||
* Field mapping allows site builders to map media item-related metadata to
|
||||
* entity fields. This information will be used when saving a given media item
|
||||
* and if metadata values will be available they are going to be automatically
|
||||
* copied to the corresponding entity fields.
|
||||
*
|
||||
* @return array
|
||||
* Field mapping array provided by media source with metadata attribute
|
||||
* names as keys and entity field names as values.
|
||||
*/
|
||||
public function getFieldMap();
|
||||
|
||||
/**
|
||||
* Sets the metadata field map.
|
||||
*
|
||||
* @param array $map
|
||||
* Field mapping array with metadata attribute names as keys and entity
|
||||
* field names as values.
|
||||
*
|
||||
* @return $this
|
||||
*/
|
||||
public function setFieldMap(array $map);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media;
|
||||
|
||||
use Drupal\Core\Config\Entity\ConfigEntityListBuilder;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Url;
|
||||
|
||||
/**
|
||||
* Provides a listing of media types.
|
||||
*/
|
||||
class MediaTypeListBuilder extends ConfigEntityListBuilder {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildHeader() {
|
||||
$header['title'] = $this->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. <a href=":url">Add media type</a>.', [
|
||||
':url' => Url::fromRoute('entity.media_type.add_form')->toString(),
|
||||
]);
|
||||
return $build;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media;
|
||||
|
||||
use Drupal\views\EntityViewsData;
|
||||
|
||||
/**
|
||||
* Provides the Views data for the media entity type.
|
||||
*/
|
||||
class MediaViewsData extends EntityViewsData {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getViewsData() {
|
||||
$data = parent::getViewsData();
|
||||
|
||||
$data['media_field_data']['table']['wizard_id'] = 'media';
|
||||
$data['media_field_revision']['table']['wizard_id'] = 'media_revision';
|
||||
|
||||
$data['media_field_data']['status_extra'] = [
|
||||
'title' => $this->t('Published status or admin user'),
|
||||
'help' => $this->t('Filters out unpublished media if the current user cannot view it.'),
|
||||
'filter' => [
|
||||
'field' => 'status',
|
||||
'id' => 'media_status',
|
||||
'label' => $this->t('Published status or admin user'),
|
||||
],
|
||||
];
|
||||
|
||||
return $data;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,176 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media\OEmbed;
|
||||
|
||||
use Drupal\Component\Utility\UrlHelper;
|
||||
|
||||
/**
|
||||
* Value object for oEmbed provider endpoints.
|
||||
*
|
||||
* @internal
|
||||
* This class is an internal part of the oEmbed system and should only be
|
||||
* instantiated by instances of Drupal\media\OEmbed\Provider.
|
||||
*/
|
||||
class Endpoint {
|
||||
|
||||
/**
|
||||
* The endpoint's URL.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $url;
|
||||
|
||||
/**
|
||||
* The provider this endpoint belongs to.
|
||||
*
|
||||
* @var \Drupal\media\OEmbed\Provider
|
||||
*/
|
||||
protected $provider;
|
||||
|
||||
/**
|
||||
* List of URL schemes supported by the provider.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $schemes;
|
||||
|
||||
/**
|
||||
* List of supported formats. Only 'json' and 'xml' are allowed.
|
||||
*
|
||||
* @var string[]
|
||||
*
|
||||
* @see https://oembed.com/#section2
|
||||
*/
|
||||
protected $formats;
|
||||
|
||||
/**
|
||||
* Whether the provider supports oEmbed discovery.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
protected $supportsDiscovery;
|
||||
|
||||
/**
|
||||
* Endpoint constructor.
|
||||
*
|
||||
* @param string $url
|
||||
* The endpoint URL. May contain a @code '{format}' @endcode placeholder.
|
||||
* @param \Drupal\media\OEmbed\Provider $provider
|
||||
* The provider this endpoint belongs to.
|
||||
* @param string[] $schemes
|
||||
* List of URL schemes supported by the provider.
|
||||
* @param string[] $formats
|
||||
* List of supported formats. Can be "json", "xml" or both.
|
||||
* @param bool $supports_discovery
|
||||
* Whether the provider supports oEmbed discovery.
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
* If the endpoint URL is empty.
|
||||
*/
|
||||
public function __construct($url, Provider $provider, array $schemes = [], array $formats = [], $supports_discovery = FALSE) {
|
||||
$this->provider = $provider;
|
||||
$this->schemes = array_map('mb_strtolower', $schemes);
|
||||
|
||||
$this->formats = $formats = array_map('mb_strtolower', $formats);
|
||||
// Assert that only the supported formats are present.
|
||||
assert(array_diff($formats, ['json', 'xml']) == []);
|
||||
|
||||
// Use the first provided format to build the endpoint URL. If no formats
|
||||
// are provided, default to JSON.
|
||||
$this->url = str_replace('{format}', reset($this->formats) ?: 'json', $url);
|
||||
|
||||
if (!UrlHelper::isValid($this->url, TRUE) || !UrlHelper::isExternal($this->url)) {
|
||||
throw new \InvalidArgumentException('oEmbed endpoint must have a valid external URL');
|
||||
}
|
||||
|
||||
$this->supportsDiscovery = (bool) $supports_discovery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the endpoint URL.
|
||||
*
|
||||
* The URL will be built with the first available format. If the endpoint
|
||||
* does not provide any formats, JSON will be used.
|
||||
*
|
||||
* @return string
|
||||
* The endpoint URL.
|
||||
*/
|
||||
public function getUrl() {
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the provider this endpoint belongs to.
|
||||
*
|
||||
* @return \Drupal\media\OEmbed\Provider
|
||||
* The provider object.
|
||||
*/
|
||||
public function getProvider() {
|
||||
return $this->provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of URL schemes supported by the provider.
|
||||
*
|
||||
* @return string[]
|
||||
* List of schemes.
|
||||
*/
|
||||
public function getSchemes() {
|
||||
return $this->schemes;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns list of supported formats.
|
||||
*
|
||||
* @return string[]
|
||||
* List of formats.
|
||||
*/
|
||||
public function getFormats() {
|
||||
return $this->formats;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns whether the provider supports oEmbed discovery.
|
||||
*
|
||||
* @return bool
|
||||
* Returns TRUE if the provides discovery, otherwise FALSE.
|
||||
*/
|
||||
public function supportsDiscovery() {
|
||||
return $this->supportsDiscovery;
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to match a URL against the endpoint schemes.
|
||||
*
|
||||
* @param string $url
|
||||
* Media item URL.
|
||||
*
|
||||
* @return bool
|
||||
* TRUE if the URL matches against the endpoint schemes, otherwise FALSE.
|
||||
*/
|
||||
public function matchUrl($url) {
|
||||
foreach ($this->getSchemes() as $scheme) {
|
||||
// Convert scheme into a valid regular expression.
|
||||
$regexp = str_replace(['.', '*'], ['\.', '.*'], $scheme);
|
||||
if (preg_match("|^$regexp$|", $url)) {
|
||||
return TRUE;
|
||||
}
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds and returns the endpoint URL.
|
||||
*
|
||||
* @param string $url
|
||||
* The canonical media URL.
|
||||
*
|
||||
* @return string
|
||||
* URL of the oEmbed endpoint.
|
||||
*/
|
||||
public function buildResourceUrl($url) {
|
||||
$query = ['url' => $url];
|
||||
return $this->getUrl() . '?' . UrlHelper::buildQuery($query);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media\OEmbed;
|
||||
|
||||
use Drupal\Component\Utility\UrlHelper;
|
||||
|
||||
/**
|
||||
* Value object for oEmbed providers.
|
||||
*/
|
||||
class Provider {
|
||||
|
||||
/**
|
||||
* The provider name.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $name;
|
||||
|
||||
/**
|
||||
* The provider URL.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $url;
|
||||
|
||||
/**
|
||||
* The provider endpoints.
|
||||
*
|
||||
* @var \Drupal\media\OEmbed\Endpoint[]
|
||||
*/
|
||||
protected $endpoints = [];
|
||||
|
||||
/**
|
||||
* Provider constructor.
|
||||
*
|
||||
* @param string $name
|
||||
* The provider name.
|
||||
* @param string $url
|
||||
* The provider URL.
|
||||
* @param array[] $endpoints
|
||||
* List of endpoints this provider exposes.
|
||||
*
|
||||
* @throws \Drupal\media\OEmbed\ProviderException
|
||||
*/
|
||||
public function __construct($name, $url, array $endpoints) {
|
||||
if (!UrlHelper::isValid($url, TRUE) || !UrlHelper::isExternal($url)) {
|
||||
throw new ProviderException('Provider @name does not define a valid external URL.', $this);
|
||||
}
|
||||
|
||||
$this->name = $name;
|
||||
$this->url = $url;
|
||||
|
||||
try {
|
||||
foreach ($endpoints as $endpoint) {
|
||||
$endpoint += ['formats' => [], 'schemes' => [], 'discovery' => FALSE];
|
||||
$this->endpoints[] = new Endpoint($endpoint['url'], $this, $endpoint['schemes'], $endpoint['formats'], $endpoint['discovery']);
|
||||
}
|
||||
}
|
||||
catch (\InvalidArgumentException $e) {
|
||||
// Just skip all the invalid endpoints.
|
||||
// @todo Log the exception message to help with debugging in
|
||||
// https://www.drupal.org/project/drupal/issues/2972846.
|
||||
}
|
||||
|
||||
if (empty($this->endpoints)) {
|
||||
throw new ProviderException('Provider @name does not define any valid endpoints.', $this);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the provider name.
|
||||
*
|
||||
* @return string
|
||||
* Name of the provider.
|
||||
*/
|
||||
public function getName() {
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the provider URL.
|
||||
*
|
||||
* @return string
|
||||
* URL of the provider.
|
||||
*/
|
||||
public function getUrl() {
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the provider endpoints.
|
||||
*
|
||||
* @return \Drupal\media\OEmbed\Endpoint[]
|
||||
* List of endpoints this provider exposes.
|
||||
*/
|
||||
public function getEndpoints() {
|
||||
return $this->endpoints;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media\OEmbed;
|
||||
|
||||
/**
|
||||
* Exception thrown if an oEmbed provider causes an error.
|
||||
*
|
||||
* @internal
|
||||
* This is an internal part of the oEmbed system and should only be used by
|
||||
* oEmbed-related code in Drupal core.
|
||||
*/
|
||||
class ProviderException extends \Exception {
|
||||
|
||||
/**
|
||||
* Information about the oEmbed provider which caused the exception.
|
||||
*
|
||||
* @var \Drupal\media\OEmbed\Provider
|
||||
*
|
||||
* @see \Drupal\media\OEmbed\ProviderRepositoryInterface::get()
|
||||
*/
|
||||
protected $provider;
|
||||
|
||||
/**
|
||||
* ProviderException constructor.
|
||||
*
|
||||
* @param string $message
|
||||
* The exception message. '@name' will be replaced with the provider name
|
||||
* if available, or '<unknown>' if not.
|
||||
* @param \Drupal\media\OEmbed\Provider $provider
|
||||
* (optional) The provider information.
|
||||
* @param \Exception $previous
|
||||
* (optional) The previous exception, if any.
|
||||
*/
|
||||
public function __construct($message, Provider $provider = NULL, \Exception $previous = NULL) {
|
||||
$this->provider = $provider;
|
||||
$message = str_replace('@name', $provider ? $provider->getName() : '<unknown>', $message);
|
||||
parent::__construct($message, 0, $previous);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media\OEmbed;
|
||||
|
||||
use Drupal\Component\Datetime\TimeInterface;
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\Core\Cache\CacheBackendInterface;
|
||||
use Drupal\Core\Cache\UseCacheBackendTrait;
|
||||
use Drupal\Core\Config\ConfigFactoryInterface;
|
||||
use GuzzleHttp\ClientInterface;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
|
||||
/**
|
||||
* Retrieves and caches information about oEmbed providers.
|
||||
*/
|
||||
class ProviderRepository implements ProviderRepositoryInterface {
|
||||
|
||||
use UseCacheBackendTrait;
|
||||
|
||||
/**
|
||||
* How long the provider data should be cached, in seconds.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $maxAge;
|
||||
|
||||
/**
|
||||
* The HTTP client.
|
||||
*
|
||||
* @var \GuzzleHttp\Client
|
||||
*/
|
||||
protected $httpClient;
|
||||
|
||||
/**
|
||||
* URL of a JSON document which contains a database of oEmbed providers.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $providersUrl;
|
||||
|
||||
/**
|
||||
* The time service.
|
||||
*
|
||||
* @var \Drupal\Component\Datetime\TimeInterface
|
||||
*/
|
||||
protected $time;
|
||||
|
||||
/**
|
||||
* Constructs a ProviderRepository instance.
|
||||
*
|
||||
* @param \GuzzleHttp\ClientInterface $http_client
|
||||
* The HTTP client.
|
||||
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
|
||||
* The config factory service.
|
||||
* @param \Drupal\Component\Datetime\TimeInterface $time
|
||||
* The time service.
|
||||
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
|
||||
* (optional) The cache backend.
|
||||
* @param int $max_age
|
||||
* (optional) How long the cache data should be kept. Defaults to a week.
|
||||
*/
|
||||
public function __construct(ClientInterface $http_client, ConfigFactoryInterface $config_factory, TimeInterface $time, CacheBackendInterface $cache_backend = NULL, $max_age = 604800) {
|
||||
$this->httpClient = $http_client;
|
||||
$this->providersUrl = $config_factory->get('media.settings')->get('oembed_providers_url');
|
||||
$this->time = $time;
|
||||
$this->cacheBackend = $cache_backend;
|
||||
$this->maxAge = (int) $max_age;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getAll() {
|
||||
$cache_id = 'media:oembed_providers';
|
||||
|
||||
$cached = $this->cacheGet($cache_id);
|
||||
if ($cached) {
|
||||
return $cached->data;
|
||||
}
|
||||
|
||||
try {
|
||||
$response = $this->httpClient->request('GET', $this->providersUrl);
|
||||
}
|
||||
catch (RequestException $e) {
|
||||
throw new ProviderException("Could not retrieve the oEmbed provider database from $this->providersUrl", NULL, $e);
|
||||
}
|
||||
|
||||
$providers = Json::decode((string) $response->getBody());
|
||||
|
||||
if (!is_array($providers) || empty($providers)) {
|
||||
throw new ProviderException('Remote oEmbed providers database returned invalid or empty list.');
|
||||
}
|
||||
|
||||
$keyed_providers = [];
|
||||
foreach ($providers as $provider) {
|
||||
try {
|
||||
$name = (string) $provider['provider_name'];
|
||||
$keyed_providers[$name] = new Provider($provider['provider_name'], $provider['provider_url'], $provider['endpoints']);
|
||||
}
|
||||
catch (ProviderException $e) {
|
||||
// Just skip all the invalid providers.
|
||||
// @todo Log the exception message to help with debugging.
|
||||
}
|
||||
}
|
||||
|
||||
$this->cacheSet($cache_id, $keyed_providers, $this->time->getCurrentTime() + $this->maxAge);
|
||||
return $keyed_providers;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function get($provider_name) {
|
||||
$providers = $this->getAll();
|
||||
|
||||
if (!isset($providers[$provider_name])) {
|
||||
throw new \InvalidArgumentException("Unknown provider '$provider_name'");
|
||||
}
|
||||
return $providers[$provider_name];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media\OEmbed;
|
||||
|
||||
/**
|
||||
* Defines an interface for a collection of oEmbed provider information.
|
||||
*
|
||||
* The provider repository is responsible for fetching information about all
|
||||
* available oEmbed providers, most likely pulled from the online database at
|
||||
* https://oembed.com/providers.json, and creating \Drupal\media\OEmbed\Provider
|
||||
* value objects for each provider.
|
||||
*/
|
||||
interface ProviderRepositoryInterface {
|
||||
|
||||
/**
|
||||
* Returns information on all available oEmbed providers.
|
||||
*
|
||||
* @return \Drupal\media\OEmbed\Provider[]
|
||||
* Returns an array of provider value objects, keyed by provider name.
|
||||
*
|
||||
* @throws \Drupal\media\OEmbed\ProviderException
|
||||
* If the oEmbed provider information cannot be retrieved.
|
||||
*/
|
||||
public function getAll();
|
||||
|
||||
/**
|
||||
* Returns information for a specific oEmbed provider.
|
||||
*
|
||||
* @param string $provider_name
|
||||
* The name of the provider.
|
||||
*
|
||||
* @return \Drupal\media\OEmbed\Provider
|
||||
* A value object containing information about the provider.
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
* If there is no known oEmbed provider with the specified name.
|
||||
*/
|
||||
public function get($provider_name);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,529 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media\OEmbed;
|
||||
|
||||
use Drupal\Core\Cache\Cache;
|
||||
use Drupal\Core\Cache\CacheableDependencyInterface;
|
||||
use Drupal\Core\Cache\CacheableDependencyTrait;
|
||||
use Drupal\Core\Url;
|
||||
|
||||
/**
|
||||
* Value object representing an oEmbed resource.
|
||||
*
|
||||
* Data received from an oEmbed provider could be insecure. For example,
|
||||
* resources of the 'rich' type provide an HTML representation which is not
|
||||
* sanitized by this object in any way. Any values you retrieve from this object
|
||||
* should be treated as potentially dangerous user input and carefully validated
|
||||
* and sanitized before being displayed or otherwise manipulated by your code.
|
||||
*
|
||||
* Valid resource types are defined in the oEmbed specification and represented
|
||||
* by the TYPE_* constants in this class.
|
||||
*
|
||||
* @see https://oembed.com/#section2
|
||||
*
|
||||
* @internal
|
||||
* This class is an internal part of the oEmbed system and should only be
|
||||
* instantiated by
|
||||
* \Drupal\media\OEmbed\ResourceFetcherInterface::fetchResource().
|
||||
*/
|
||||
class Resource implements CacheableDependencyInterface {
|
||||
|
||||
use CacheableDependencyTrait;
|
||||
|
||||
/**
|
||||
* The resource type for link resources.
|
||||
*/
|
||||
const TYPE_LINK = 'link';
|
||||
|
||||
/**
|
||||
* The resource type for photo resources.
|
||||
*/
|
||||
const TYPE_PHOTO = 'photo';
|
||||
|
||||
/**
|
||||
* The resource type for rich resources.
|
||||
*/
|
||||
const TYPE_RICH = 'rich';
|
||||
|
||||
/**
|
||||
* The resource type for video resources.
|
||||
*/
|
||||
const TYPE_VIDEO = 'video';
|
||||
|
||||
/**
|
||||
* The resource type. Can be one of the static::TYPE_* constants.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $type;
|
||||
|
||||
/**
|
||||
* The resource provider.
|
||||
*
|
||||
* @var \Drupal\media\OEmbed\Provider
|
||||
*/
|
||||
protected $provider;
|
||||
|
||||
/**
|
||||
* A text title, describing the resource.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $title;
|
||||
|
||||
/**
|
||||
* The name of the author/owner of the resource.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $authorName;
|
||||
|
||||
/**
|
||||
* A URL for the author/owner of the resource.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $authorUrl;
|
||||
|
||||
/**
|
||||
* A URL to a thumbnail image representing the resource.
|
||||
*
|
||||
* The thumbnail must respect any maxwidth and maxheight parameters passed
|
||||
* to the oEmbed endpoint. If this parameter is present, thumbnail_width and
|
||||
* thumbnail_height must also be present.
|
||||
*
|
||||
* @var string
|
||||
*
|
||||
* @see \Drupal\media\OEmbed\UrlResolverInterface::getResourceUrl()
|
||||
* @see https://oembed.com/#section2
|
||||
*/
|
||||
protected $thumbnailUrl;
|
||||
|
||||
/**
|
||||
* The width of the thumbnail, in pixels.
|
||||
*
|
||||
* If this parameter is present, thumbnail_url and thumbnail_height must also
|
||||
* be present.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $thumbnailWidth;
|
||||
|
||||
/**
|
||||
* The height of the thumbnail, in pixels.
|
||||
*
|
||||
* If this parameter is present, thumbnail_url and thumbnail_width must also
|
||||
* be present.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $thumbnailHeight;
|
||||
|
||||
/**
|
||||
* The width of the resource, in pixels.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $width;
|
||||
|
||||
/**
|
||||
* The height of the resource, in pixels.
|
||||
*
|
||||
* @var int
|
||||
*/
|
||||
protected $height;
|
||||
|
||||
/**
|
||||
* The resource URL. Only applies to 'photo' and 'link' resources.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $url;
|
||||
|
||||
/**
|
||||
* The HTML representation of the resource.
|
||||
*
|
||||
* Only applies to 'rich' and 'video' resources.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $html;
|
||||
|
||||
/**
|
||||
* Resource constructor.
|
||||
*
|
||||
* @param \Drupal\media\OEmbed\Provider $provider
|
||||
* (optional) The resource provider.
|
||||
* @param string $title
|
||||
* (optional) A text title, describing the resource.
|
||||
* @param string $author_name
|
||||
* (optional) The name of the author/owner of the resource.
|
||||
* @param string $author_url
|
||||
* (optional) A URL for the author/owner of the resource.
|
||||
* @param int $cache_age
|
||||
* (optional) The suggested cache lifetime for this resource, in seconds.
|
||||
* @param string $thumbnail_url
|
||||
* (optional) A URL to a thumbnail image representing the resource. If this
|
||||
* parameter is present, $thumbnail_width and $thumbnail_height must also be
|
||||
* present.
|
||||
* @param int $thumbnail_width
|
||||
* (optional) The width of the thumbnail, in pixels. If this parameter is
|
||||
* present, $thumbnail_url and $thumbnail_height must also be present.
|
||||
* @param int $thumbnail_height
|
||||
* (optional) The height of the thumbnail, in pixels. If this parameter is
|
||||
* present, $thumbnail_url and $thumbnail_width must also be present.
|
||||
*/
|
||||
protected function __construct(Provider $provider = NULL, $title = NULL, $author_name = NULL, $author_url = NULL, $cache_age = NULL, $thumbnail_url = NULL, $thumbnail_width = NULL, $thumbnail_height = NULL) {
|
||||
$this->provider = $provider;
|
||||
$this->title = $title;
|
||||
$this->authorName = $author_name;
|
||||
$this->authorUrl = $author_url;
|
||||
|
||||
if (isset($cache_age) && is_numeric($cache_age)) {
|
||||
// If the cache age is too big, it can overflow the 'expire' column of
|
||||
// database cache backends, causing SQL exceptions. To prevent that,
|
||||
// arbitrarily limit the cache age to 5 years. That should be enough.
|
||||
$this->cacheMaxAge = Cache::mergeMaxAges((int) $cache_age, 157680000);
|
||||
}
|
||||
|
||||
if ($thumbnail_url) {
|
||||
$this->thumbnailUrl = $thumbnail_url;
|
||||
$this->setThumbnailDimensions($thumbnail_width, $thumbnail_height);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a link resource.
|
||||
*
|
||||
* @param string $url
|
||||
* (optional) The URL of the resource.
|
||||
* @param \Drupal\media\OEmbed\Provider $provider
|
||||
* (optional) The resource provider.
|
||||
* @param string $title
|
||||
* (optional) A text title, describing the resource.
|
||||
* @param string $author_name
|
||||
* (optional) The name of the author/owner of the resource.
|
||||
* @param string $author_url
|
||||
* (optional) A URL for the author/owner of the resource.
|
||||
* @param int $cache_age
|
||||
* (optional) The suggested cache lifetime for this resource, in seconds.
|
||||
* @param string $thumbnail_url
|
||||
* (optional) A URL to a thumbnail image representing the resource. If this
|
||||
* parameter is present, $thumbnail_width and $thumbnail_height must also be
|
||||
* present.
|
||||
* @param int $thumbnail_width
|
||||
* (optional) The width of the thumbnail, in pixels. If this parameter is
|
||||
* present, $thumbnail_url and $thumbnail_height must also be present.
|
||||
* @param int $thumbnail_height
|
||||
* (optional) The height of the thumbnail, in pixels. If this parameter is
|
||||
* present, $thumbnail_url and $thumbnail_width must also be present.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public static function link($url = NULL, Provider $provider = NULL, $title = NULL, $author_name = NULL, $author_url = NULL, $cache_age = NULL, $thumbnail_url = NULL, $thumbnail_width = NULL, $thumbnail_height = NULL) {
|
||||
$resource = new static($provider, $title, $author_name, $author_url, $cache_age, $thumbnail_url, $thumbnail_width, $thumbnail_height);
|
||||
$resource->type = self::TYPE_LINK;
|
||||
$resource->url = $url;
|
||||
|
||||
return $resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a photo resource.
|
||||
*
|
||||
* @param string $url
|
||||
* The URL of the photo.
|
||||
* @param int $width
|
||||
* The width of the photo, in pixels.
|
||||
* @param int $height
|
||||
* The height of the photo, in pixels.
|
||||
* @param \Drupal\media\OEmbed\Provider $provider
|
||||
* (optional) The resource provider.
|
||||
* @param string $title
|
||||
* (optional) A text title, describing the resource.
|
||||
* @param string $author_name
|
||||
* (optional) The name of the author/owner of the resource.
|
||||
* @param string $author_url
|
||||
* (optional) A URL for the author/owner of the resource.
|
||||
* @param int $cache_age
|
||||
* (optional) The suggested cache lifetime for this resource, in seconds.
|
||||
* @param string $thumbnail_url
|
||||
* (optional) A URL to a thumbnail image representing the resource. If this
|
||||
* parameter is present, $thumbnail_width and $thumbnail_height must also be
|
||||
* present.
|
||||
* @param int $thumbnail_width
|
||||
* (optional) The width of the thumbnail, in pixels. If this parameter is
|
||||
* present, $thumbnail_url and $thumbnail_height must also be present.
|
||||
* @param int $thumbnail_height
|
||||
* (optional) The height of the thumbnail, in pixels. If this parameter is
|
||||
* present, $thumbnail_url and $thumbnail_width must also be present.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public static function photo($url, $width, $height, Provider $provider = NULL, $title = NULL, $author_name = NULL, $author_url = NULL, $cache_age = NULL, $thumbnail_url = NULL, $thumbnail_width = NULL, $thumbnail_height = NULL) {
|
||||
if (empty($url)) {
|
||||
throw new \InvalidArgumentException('Photo resources must provide a URL.');
|
||||
}
|
||||
|
||||
$resource = static::link($url, $provider, $title, $author_name, $author_url, $cache_age, $thumbnail_url, $thumbnail_width, $thumbnail_height);
|
||||
$resource->type = self::TYPE_PHOTO;
|
||||
$resource->setDimensions($width, $height);
|
||||
|
||||
return $resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a rich resource.
|
||||
*
|
||||
* @param string $html
|
||||
* The HTML representation of the resource.
|
||||
* @param int $width
|
||||
* The width of the resource, in pixels.
|
||||
* @param int $height
|
||||
* The height of the resource, in pixels.
|
||||
* @param \Drupal\media\OEmbed\Provider $provider
|
||||
* (optional) The resource provider.
|
||||
* @param string $title
|
||||
* (optional) A text title, describing the resource.
|
||||
* @param string $author_name
|
||||
* (optional) The name of the author/owner of the resource.
|
||||
* @param string $author_url
|
||||
* (optional) A URL for the author/owner of the resource.
|
||||
* @param int $cache_age
|
||||
* (optional) The suggested cache lifetime for this resource, in seconds.
|
||||
* @param string $thumbnail_url
|
||||
* (optional) A URL to a thumbnail image representing the resource. If this
|
||||
* parameter is present, $thumbnail_width and $thumbnail_height must also be
|
||||
* present.
|
||||
* @param int $thumbnail_width
|
||||
* (optional) The width of the thumbnail, in pixels. If this parameter is
|
||||
* present, $thumbnail_url and $thumbnail_height must also be present.
|
||||
* @param int $thumbnail_height
|
||||
* (optional) The height of the thumbnail, in pixels. If this parameter is
|
||||
* present, $thumbnail_url and $thumbnail_width must also be present.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public static function rich($html, $width, $height, Provider $provider = NULL, $title = NULL, $author_name = NULL, $author_url = NULL, $cache_age = NULL, $thumbnail_url = NULL, $thumbnail_width = NULL, $thumbnail_height = NULL) {
|
||||
if (empty($html)) {
|
||||
throw new \InvalidArgumentException('The resource must provide an HTML representation.');
|
||||
}
|
||||
|
||||
$resource = new static($provider, $title, $author_name, $author_url, $cache_age, $thumbnail_url, $thumbnail_width, $thumbnail_height);
|
||||
$resource->type = self::TYPE_RICH;
|
||||
$resource->html = $html;
|
||||
$resource->setDimensions($width, $height);
|
||||
|
||||
return $resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a video resource.
|
||||
*
|
||||
* @param string $html
|
||||
* The HTML required to display the video.
|
||||
* @param int $width
|
||||
* The width of the video, in pixels.
|
||||
* @param int $height
|
||||
* The height of the video, in pixels.
|
||||
* @param \Drupal\media\OEmbed\Provider $provider
|
||||
* (optional) The resource provider.
|
||||
* @param string $title
|
||||
* (optional) A text title, describing the resource.
|
||||
* @param string $author_name
|
||||
* (optional) The name of the author/owner of the resource.
|
||||
* @param string $author_url
|
||||
* (optional) A URL for the author/owner of the resource.
|
||||
* @param int $cache_age
|
||||
* (optional) The suggested cache lifetime for this resource, in seconds.
|
||||
* @param string $thumbnail_url
|
||||
* (optional) A URL to a thumbnail image representing the resource. If this
|
||||
* parameter is present, $thumbnail_width and $thumbnail_height must also be
|
||||
* present.
|
||||
* @param int $thumbnail_width
|
||||
* (optional) The width of the thumbnail, in pixels. If this parameter is
|
||||
* present, $thumbnail_url and $thumbnail_height must also be present.
|
||||
* @param int $thumbnail_height
|
||||
* (optional) The height of the thumbnail, in pixels. If this parameter is
|
||||
* present, $thumbnail_url and $thumbnail_width must also be present.
|
||||
*
|
||||
* @return static
|
||||
*/
|
||||
public static function video($html, $width, $height, Provider $provider = NULL, $title = NULL, $author_name = NULL, $author_url = NULL, $cache_age = NULL, $thumbnail_url = NULL, $thumbnail_width = NULL, $thumbnail_height = NULL) {
|
||||
$resource = static::rich($html, $width, $height, $provider, $title, $author_name, $author_url, $cache_age, $thumbnail_url, $thumbnail_width, $thumbnail_height);
|
||||
$resource->type = self::TYPE_VIDEO;
|
||||
|
||||
return $resource;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the resource type.
|
||||
*
|
||||
* @return string
|
||||
* The resource type. Will be one of the self::TYPE_* constants.
|
||||
*/
|
||||
public function getType() {
|
||||
return $this->type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the title of the resource.
|
||||
*
|
||||
* @return string|null
|
||||
* The title of the resource, if known.
|
||||
*/
|
||||
public function getTitle() {
|
||||
return $this->title;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the name of the resource author.
|
||||
*
|
||||
* @return string|null
|
||||
* The name of the resource author, if known.
|
||||
*/
|
||||
public function getAuthorName() {
|
||||
return $this->authorName;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URL of the resource author.
|
||||
*
|
||||
* @return \Drupal\Core\Url|null
|
||||
* The absolute URL of the resource author, or NULL if none is provided.
|
||||
*/
|
||||
public function getAuthorUrl() {
|
||||
return $this->authorUrl ? Url::fromUri($this->authorUrl)->setAbsolute() : NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the resource provider, if known.
|
||||
*
|
||||
* @return \Drupal\media\OEmbed\Provider|null
|
||||
* The resource provider, or NULL if the provider is not known.
|
||||
*/
|
||||
public function getProvider() {
|
||||
return $this->provider;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URL of the resource's thumbnail image.
|
||||
*
|
||||
* @return \Drupal\Core\Url|null
|
||||
* The absolute URL of the thumbnail image, or NULL if there isn't one.
|
||||
*/
|
||||
public function getThumbnailUrl() {
|
||||
return $this->thumbnailUrl ? Url::fromUri($this->thumbnailUrl)->setAbsolute() : NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the width of the resource's thumbnail image.
|
||||
*
|
||||
* @return int|null
|
||||
* The thumbnail width in pixels, or NULL if there is no thumbnail.
|
||||
*/
|
||||
public function getThumbnailWidth() {
|
||||
return $this->thumbnailWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the height of the resource's thumbnail image.
|
||||
*
|
||||
* @return int|null
|
||||
* The thumbnail height in pixels, or NULL if there is no thumbnail.
|
||||
*/
|
||||
public function getThumbnailHeight() {
|
||||
return $this->thumbnailHeight;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the width of the resource.
|
||||
*
|
||||
* @return int|null
|
||||
* The width of the resource in pixels, or NULL if the resource has no
|
||||
* width.
|
||||
*/
|
||||
public function getWidth() {
|
||||
return $this->width;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the height of the resource.
|
||||
*
|
||||
* @return int|null
|
||||
* The height of the resource in pixels, or NULL if the resource has no
|
||||
* height.
|
||||
*/
|
||||
public function getHeight() {
|
||||
return $this->height;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the URL of the resource. Only applies to 'photo' resources.
|
||||
*
|
||||
* @return \Drupal\Core\Url|null
|
||||
* The resource URL, if it has one.
|
||||
*/
|
||||
public function getUrl() {
|
||||
if ($this->url) {
|
||||
return Url::fromUri($this->url)->setAbsolute();
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the HTML representation of the resource.
|
||||
*
|
||||
* Only applies to 'rich' and 'video' resources.
|
||||
*
|
||||
* @return string|null
|
||||
* The HTML representation of the resource, if it has one.
|
||||
*/
|
||||
public function getHtml() {
|
||||
return isset($this->html) ? (string) $this->html : NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the thumbnail dimensions.
|
||||
*
|
||||
* @param int $width
|
||||
* The width of the resource.
|
||||
* @param int $height
|
||||
* The height of the resource.
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
* If either $width or $height are not numbers greater than zero.
|
||||
*/
|
||||
protected function setThumbnailDimensions($width, $height) {
|
||||
$width = (int) $width;
|
||||
$height = (int) $height;
|
||||
|
||||
if ($width > 0 && $height > 0) {
|
||||
$this->thumbnailWidth = $width;
|
||||
$this->thumbnailHeight = $height;
|
||||
}
|
||||
else {
|
||||
throw new \InvalidArgumentException('The thumbnail dimensions must be numbers greater than zero.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the dimensions.
|
||||
*
|
||||
* @param int|null $width
|
||||
* The width of the resource.
|
||||
* @param int|null $height
|
||||
* The height of the resource.
|
||||
*
|
||||
* @throws \InvalidArgumentException
|
||||
* If either $width or $height are not numbers greater than zero.
|
||||
*/
|
||||
protected function setDimensions($width, $height) {
|
||||
if ((isset($width) && $width <= 0) || (isset($height) && $height <= 0)) {
|
||||
throw new \InvalidArgumentException('The dimensions must be NULL or numbers greater than zero.');
|
||||
}
|
||||
$this->width = isset($width) ? (int) $width : NULL;
|
||||
$this->height = isset($height) ? (int) $height : NULL;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media\OEmbed;
|
||||
|
||||
/**
|
||||
* Exception thrown if an oEmbed resource cannot be fetched or parsed.
|
||||
*
|
||||
* @internal
|
||||
* This is an internal part of the oEmbed system and should only be used by
|
||||
* oEmbed-related code in Drupal core.
|
||||
*/
|
||||
class ResourceException extends \Exception {
|
||||
|
||||
/**
|
||||
* The URL of the resource.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $url;
|
||||
|
||||
/**
|
||||
* The resource data.
|
||||
*
|
||||
* @var array
|
||||
*/
|
||||
protected $data = [];
|
||||
|
||||
/**
|
||||
* ResourceException constructor.
|
||||
*
|
||||
* @param string $message
|
||||
* The exception message.
|
||||
* @param string $url
|
||||
* The URL of the resource. Can be the actual endpoint URL or the canonical
|
||||
* URL.
|
||||
* @param array $data
|
||||
* (optional) The raw resource data, if available.
|
||||
* @param \Exception $previous
|
||||
* (optional) The previous exception, if any.
|
||||
*/
|
||||
public function __construct($message, $url, array $data = [], \Exception $previous = NULL) {
|
||||
$this->url = $url;
|
||||
$this->data = $data;
|
||||
parent::__construct($message, 0, $previous);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the URL of the resource which caused the exception.
|
||||
*
|
||||
* @return string
|
||||
* The URL of the resource.
|
||||
*/
|
||||
public function getUrl() {
|
||||
return $this->url;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the raw resource data, if available.
|
||||
*
|
||||
* @return array
|
||||
* The resource data.
|
||||
*/
|
||||
public function getData() {
|
||||
return $this->data;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media\OEmbed;
|
||||
|
||||
use Drupal\Component\Serialization\Json;
|
||||
use Drupal\Core\Cache\CacheBackendInterface;
|
||||
use Drupal\Core\Cache\UseCacheBackendTrait;
|
||||
use GuzzleHttp\ClientInterface;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
|
||||
/**
|
||||
* Fetches and caches oEmbed resources.
|
||||
*/
|
||||
class ResourceFetcher implements ResourceFetcherInterface {
|
||||
|
||||
use UseCacheBackendTrait;
|
||||
|
||||
/**
|
||||
* The HTTP client.
|
||||
*
|
||||
* @var \GuzzleHttp\Client
|
||||
*/
|
||||
protected $httpClient;
|
||||
|
||||
/**
|
||||
* The oEmbed provider repository service.
|
||||
*
|
||||
* @var \Drupal\media\OEmbed\ProviderRepositoryInterface
|
||||
*/
|
||||
protected $providers;
|
||||
|
||||
/**
|
||||
* Constructs a ResourceFetcher object.
|
||||
*
|
||||
* @param \GuzzleHttp\ClientInterface $http_client
|
||||
* The HTTP client.
|
||||
* @param \Drupal\media\OEmbed\ProviderRepositoryInterface $providers
|
||||
* The oEmbed provider repository service.
|
||||
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
|
||||
* (optional) The cache backend.
|
||||
*/
|
||||
public function __construct(ClientInterface $http_client, ProviderRepositoryInterface $providers, CacheBackendInterface $cache_backend = NULL) {
|
||||
$this->httpClient = $http_client;
|
||||
$this->providers = $providers;
|
||||
$this->cacheBackend = $cache_backend;
|
||||
$this->useCaches = isset($cache_backend);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function fetchResource($url) {
|
||||
$cache_id = "media:oembed_resource:$url";
|
||||
|
||||
$cached = $this->cacheGet($cache_id);
|
||||
if ($cached) {
|
||||
return $this->createResource($cached->data, $url);
|
||||
}
|
||||
|
||||
try {
|
||||
$response = $this->httpClient->get($url);
|
||||
}
|
||||
catch (RequestException $e) {
|
||||
throw new ResourceException('Could not retrieve the oEmbed resource.', $url, [], $e);
|
||||
}
|
||||
|
||||
list($format) = $response->getHeader('Content-Type');
|
||||
$content = (string) $response->getBody();
|
||||
|
||||
if (strstr($format, 'text/xml') || strstr($format, 'application/xml')) {
|
||||
$data = $this->parseResourceXml($content, $url);
|
||||
}
|
||||
elseif (strstr($format, 'text/javascript') || strstr($format, 'application/json')) {
|
||||
$data = Json::decode($content);
|
||||
}
|
||||
// If the response is neither XML nor JSON, we are in bat country.
|
||||
else {
|
||||
throw new ResourceException('The fetched resource did not have a valid Content-Type header.', $url);
|
||||
}
|
||||
|
||||
$this->cacheSet($cache_id, $data);
|
||||
|
||||
return $this->createResource($data, $url);
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a Resource object from raw resource data.
|
||||
*
|
||||
* @param array $data
|
||||
* The resource data returned by the provider.
|
||||
* @param string $url
|
||||
* The URL of the resource.
|
||||
*
|
||||
* @return \Drupal\media\OEmbed\Resource
|
||||
* A value object representing the resource.
|
||||
*
|
||||
* @throws \Drupal\media\OEmbed\ResourceException
|
||||
* If the resource cannot be created.
|
||||
*/
|
||||
protected function createResource(array $data, $url) {
|
||||
$data += [
|
||||
'title' => NULL,
|
||||
'author_name' => NULL,
|
||||
'author_url' => NULL,
|
||||
'provider_name' => NULL,
|
||||
'cache_age' => NULL,
|
||||
'thumbnail_url' => NULL,
|
||||
'thumbnail_width' => NULL,
|
||||
'thumbnail_height' => NULL,
|
||||
'width' => NULL,
|
||||
'height' => NULL,
|
||||
'url' => NULL,
|
||||
'html' => NULL,
|
||||
'version' => NULL,
|
||||
];
|
||||
|
||||
if ($data['version'] !== '1.0') {
|
||||
throw new ResourceException("Resource version must be '1.0'", $url, $data);
|
||||
}
|
||||
|
||||
// Prepare the arguments to pass to the factory method.
|
||||
$provider = $data['provider_name'] ? $this->providers->get($data['provider_name']) : NULL;
|
||||
|
||||
// The Resource object will validate the data we create it with and throw an
|
||||
// exception if anything looks wrong. For better debugging, catch those
|
||||
// exceptions and wrap them in a more specific and useful exception.
|
||||
try {
|
||||
switch ($data['type']) {
|
||||
case Resource::TYPE_LINK:
|
||||
return Resource::link(
|
||||
$data['url'],
|
||||
$provider,
|
||||
$data['title'],
|
||||
$data['author_name'],
|
||||
$data['author_url'],
|
||||
$data['cache_age'],
|
||||
$data['thumbnail_url'],
|
||||
$data['thumbnail_width'],
|
||||
$data['thumbnail_height']
|
||||
);
|
||||
|
||||
case Resource::TYPE_PHOTO:
|
||||
return Resource::photo(
|
||||
$data['url'],
|
||||
$data['width'],
|
||||
$data['height'],
|
||||
$provider,
|
||||
$data['title'],
|
||||
$data['author_name'],
|
||||
$data['author_url'],
|
||||
$data['cache_age'],
|
||||
$data['thumbnail_url'],
|
||||
$data['thumbnail_width'],
|
||||
$data['thumbnail_height']
|
||||
);
|
||||
|
||||
case Resource::TYPE_RICH:
|
||||
return Resource::rich(
|
||||
$data['html'],
|
||||
$data['width'],
|
||||
$data['height'],
|
||||
$provider,
|
||||
$data['title'],
|
||||
$data['author_name'],
|
||||
$data['author_url'],
|
||||
$data['cache_age'],
|
||||
$data['thumbnail_url'],
|
||||
$data['thumbnail_width'],
|
||||
$data['thumbnail_height']
|
||||
);
|
||||
|
||||
case Resource::TYPE_VIDEO:
|
||||
return Resource::video(
|
||||
$data['html'],
|
||||
$data['width'],
|
||||
$data['height'],
|
||||
$provider,
|
||||
$data['title'],
|
||||
$data['author_name'],
|
||||
$data['author_url'],
|
||||
$data['cache_age'],
|
||||
$data['thumbnail_url'],
|
||||
$data['thumbnail_width'],
|
||||
$data['thumbnail_height']
|
||||
);
|
||||
|
||||
default:
|
||||
throw new ResourceException('Unknown resource type: ' . $data['type'], $url, $data);
|
||||
}
|
||||
}
|
||||
catch (\InvalidArgumentException $e) {
|
||||
throw new ResourceException($e->getMessage(), $url, $data, $e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parses XML resource data.
|
||||
*
|
||||
* @param string $data
|
||||
* The raw XML for the resource.
|
||||
* @param string $url
|
||||
* The resource URL.
|
||||
*
|
||||
* @return array
|
||||
* The parsed resource data.
|
||||
*
|
||||
* @throws \Drupal\media\OEmbed\ResourceException
|
||||
* If the resource data could not be parsed.
|
||||
*/
|
||||
protected function parseResourceXml($data, $url) {
|
||||
// Enable userspace error handling.
|
||||
$was_using_internal_errors = libxml_use_internal_errors(TRUE);
|
||||
libxml_clear_errors();
|
||||
|
||||
$content = simplexml_load_string($data, 'SimpleXMLElement', LIBXML_NOCDATA);
|
||||
// Restore the previous error handling behavior.
|
||||
libxml_use_internal_errors($was_using_internal_errors);
|
||||
|
||||
$error = libxml_get_last_error();
|
||||
if ($error) {
|
||||
libxml_clear_errors();
|
||||
throw new ResourceException($error->message, $url);
|
||||
}
|
||||
elseif ($content === FALSE) {
|
||||
throw new ResourceException('The fetched resource could not be parsed.', $url);
|
||||
}
|
||||
|
||||
// Convert XML to JSON so that the parsed resource has a consistent array
|
||||
// structure, regardless of any XML attributes or quirks of the XML parser.
|
||||
$data = Json::encode($content);
|
||||
return Json::decode($data);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media\OEmbed;
|
||||
|
||||
/**
|
||||
* Defines an interface for an oEmbed resource fetcher service.
|
||||
*
|
||||
* The resource fetcher's only responsibility is to retrieve oEmbed resource
|
||||
* data from an endpoint URL (i.e., as returned by
|
||||
* \Drupal\media\OEmbed\UrlResolverInterface::getResourceUrl()) and return a
|
||||
* \Drupal\media\OEmbed\Resource value object.
|
||||
*/
|
||||
interface ResourceFetcherInterface {
|
||||
|
||||
/**
|
||||
* Fetches an oEmbed resource.
|
||||
*
|
||||
* @param string $url
|
||||
* Endpoint-specific URL of the oEmbed resource.
|
||||
*
|
||||
* @return \Drupal\media\OEmbed\Resource
|
||||
* A resource object built from the oEmbed resource data.
|
||||
*
|
||||
* @see https://oembed.com/#section2
|
||||
*
|
||||
* @throws \Drupal\media\OEmbed\ResourceException
|
||||
* If the oEmbed endpoint is not reachable or the response returns an
|
||||
* unexpected Content-Type header.
|
||||
*/
|
||||
public function fetchResource($url);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media\OEmbed;
|
||||
|
||||
use Drupal\Component\Utility\Html;
|
||||
use Drupal\Component\Utility\UrlHelper;
|
||||
use Drupal\Core\Cache\CacheBackendInterface;
|
||||
use Drupal\Core\Cache\UseCacheBackendTrait;
|
||||
use Drupal\Core\Extension\ModuleHandlerInterface;
|
||||
use GuzzleHttp\ClientInterface;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
|
||||
/**
|
||||
* Converts oEmbed media URLs into endpoint-specific resource URLs.
|
||||
*/
|
||||
class UrlResolver implements UrlResolverInterface {
|
||||
|
||||
use UseCacheBackendTrait;
|
||||
|
||||
/**
|
||||
* The HTTP client.
|
||||
*
|
||||
* @var \GuzzleHttp\Client
|
||||
*/
|
||||
protected $httpClient;
|
||||
|
||||
/**
|
||||
* The OEmbed provider repository service.
|
||||
*
|
||||
* @var \Drupal\media\OEmbed\ProviderRepositoryInterface
|
||||
*/
|
||||
protected $providers;
|
||||
|
||||
/**
|
||||
* The OEmbed resource fetcher service.
|
||||
*
|
||||
* @var \Drupal\media\OEmbed\ResourceFetcherInterface
|
||||
*/
|
||||
protected $resourceFetcher;
|
||||
|
||||
/**
|
||||
* The module handler service.
|
||||
*
|
||||
* @var \Drupal\Core\Extension\ModuleHandlerInterface
|
||||
*/
|
||||
protected $moduleHandler;
|
||||
|
||||
/**
|
||||
* Static cache of discovered oEmbed resource URLs, keyed by canonical URL.
|
||||
*
|
||||
* A discovered resource URL is the actual endpoint URL for a specific media
|
||||
* object, fetched from its canonical URL.
|
||||
*
|
||||
* @var string[]
|
||||
*/
|
||||
protected $urlCache = [];
|
||||
|
||||
/**
|
||||
* Constructs a UrlResolver object.
|
||||
*
|
||||
* @param \Drupal\media\OEmbed\ProviderRepositoryInterface $providers
|
||||
* The oEmbed provider repository service.
|
||||
* @param \Drupal\media\OEmbed\ResourceFetcherInterface $resource_fetcher
|
||||
* The OEmbed resource fetcher service.
|
||||
* @param \GuzzleHttp\ClientInterface $http_client
|
||||
* The HTTP client.
|
||||
* @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
|
||||
* The module handler service.
|
||||
* @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend
|
||||
* (optional) The cache backend.
|
||||
*/
|
||||
public function __construct(ProviderRepositoryInterface $providers, ResourceFetcherInterface $resource_fetcher, ClientInterface $http_client, ModuleHandlerInterface $module_handler, CacheBackendInterface $cache_backend = NULL) {
|
||||
$this->providers = $providers;
|
||||
$this->resourceFetcher = $resource_fetcher;
|
||||
$this->httpClient = $http_client;
|
||||
$this->moduleHandler = $module_handler;
|
||||
$this->cacheBackend = $cache_backend;
|
||||
$this->useCaches = isset($cache_backend);
|
||||
}
|
||||
|
||||
/**
|
||||
* Runs oEmbed discovery and returns the endpoint URL if successful.
|
||||
*
|
||||
* @param string $url
|
||||
* The resource's URL.
|
||||
*
|
||||
* @return string|bool
|
||||
* URL of the oEmbed endpoint, or FALSE if the discovery was unsuccessful.
|
||||
*/
|
||||
protected function discoverResourceUrl($url) {
|
||||
try {
|
||||
$response = $this->httpClient->get($url);
|
||||
}
|
||||
catch (RequestException $e) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
$document = Html::load((string) $response->getBody());
|
||||
$xpath = new \DOMXpath($document);
|
||||
|
||||
return $this->findUrl($xpath, 'json') ?: $this->findUrl($xpath, 'xml');
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to find the oEmbed URL in a DOM.
|
||||
*
|
||||
* @param \DOMXPath $xpath
|
||||
* Page HTML as DOMXPath.
|
||||
* @param string $format
|
||||
* Format of oEmbed resource. Possible values are 'json' and 'xml'.
|
||||
*
|
||||
* @return bool|string
|
||||
* A URL to an oEmbed resource or FALSE if not found.
|
||||
*/
|
||||
protected function findUrl(\DOMXPath $xpath, $format) {
|
||||
$result = $xpath->query("//link[@type='application/$format+oembed']");
|
||||
return $result->length ? $result->item(0)->getAttribute('href') : FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getProviderByUrl($url) {
|
||||
// Check the URL against every scheme of every endpoint of every provider
|
||||
// until we find a match.
|
||||
foreach ($this->providers->getAll() as $provider_name => $provider_info) {
|
||||
foreach ($provider_info->getEndpoints() as $endpoint) {
|
||||
if ($endpoint->matchUrl($url)) {
|
||||
return $provider_info;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$resource_url = $this->discoverResourceUrl($url);
|
||||
if ($resource_url) {
|
||||
return $this->resourceFetcher->fetchResource($resource_url)->getProvider();
|
||||
}
|
||||
|
||||
throw new ResourceException('No matching provider found.', $url);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getResourceUrl($url, $max_width = NULL, $max_height = NULL) {
|
||||
// Try to get the resource URL from the static cache.
|
||||
if (isset($this->urlCache[$url])) {
|
||||
return $this->urlCache[$url];
|
||||
}
|
||||
|
||||
// Try to get the resource URL from the persistent cache.
|
||||
$cache_id = "media:oembed_resource_url:$url:$max_width:$max_height";
|
||||
|
||||
$cached = $this->cacheGet($cache_id);
|
||||
if ($cached) {
|
||||
$this->urlCache[$url] = $cached->data;
|
||||
return $this->urlCache[$url];
|
||||
}
|
||||
|
||||
$provider = $this->getProviderByUrl($url);
|
||||
$endpoints = $provider->getEndpoints();
|
||||
$endpoint = reset($endpoints);
|
||||
$resource_url = $endpoint->buildResourceUrl($url);
|
||||
|
||||
$parsed_url = UrlHelper::parse($resource_url);
|
||||
if ($max_width) {
|
||||
$parsed_url['query']['maxwidth'] = $max_width;
|
||||
}
|
||||
if ($max_height) {
|
||||
$parsed_url['query']['maxheight'] = $max_height;
|
||||
}
|
||||
// Let other modules alter the resource URL, because some oEmbed providers
|
||||
// provide extra parameters in the query string. For example, Instagram also
|
||||
// supports the 'omitscript' parameter.
|
||||
$this->moduleHandler->alter('oembed_resource_url', $parsed_url, $provider);
|
||||
$resource_url = $parsed_url['path'] . '?' . rawurldecode(UrlHelper::buildQuery($parsed_url['query']));
|
||||
|
||||
$this->urlCache[$url] = $resource_url;
|
||||
$this->cacheSet($cache_id, $resource_url);
|
||||
|
||||
return $resource_url;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media\OEmbed;
|
||||
|
||||
/**
|
||||
* Defines the interface for the oEmbed URL resolver service.
|
||||
*
|
||||
* The URL resolver is responsible for converting oEmbed-compatible media asset
|
||||
* URLs into canonical resource URLs, at which an oEmbed representation of the
|
||||
* asset can be retrieved.
|
||||
*/
|
||||
interface UrlResolverInterface {
|
||||
|
||||
/**
|
||||
* Tries to determine the oEmbed provider for a media asset URL.
|
||||
*
|
||||
* @param string $url
|
||||
* The media asset URL.
|
||||
*
|
||||
* @return \Drupal\media\OEmbed\Provider
|
||||
* The oEmbed provider for the asset.
|
||||
*
|
||||
* @throws \Drupal\media\OEmbed\ResourceException
|
||||
* If the provider cannot be determined.
|
||||
* @throws \Drupal\media\OEmbed\ProviderException
|
||||
* If tne oEmbed provider causes an error.
|
||||
*/
|
||||
public function getProviderByUrl($url);
|
||||
|
||||
/**
|
||||
* Builds the resource URL for a media asset URL.
|
||||
*
|
||||
* @param string $url
|
||||
* The media asset URL.
|
||||
* @param int $max_width
|
||||
* (optional) Maximum width of the oEmbed resource, in pixels.
|
||||
* @param int $max_height
|
||||
* (optional) Maximum height of the oEmbed resource, in pixels.
|
||||
*
|
||||
* @return string
|
||||
* Returns the resource URL corresponding to the given media item URL.
|
||||
*/
|
||||
public function getResourceUrl($url, $max_width = NULL, $max_height = NULL);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media\Plugin\CKEditorPlugin;
|
||||
|
||||
use Drupal\ckeditor\CKEditorPluginContextualInterface;
|
||||
use Drupal\ckeditor\CKEditorPluginCssInterface;
|
||||
use Drupal\Core\Extension\ModuleExtensionList;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Drupal\Core\Plugin\PluginBase;
|
||||
use Drupal\editor\Entity\Editor;
|
||||
|
||||
/**
|
||||
* Defines the "drupalmedia" plugin.
|
||||
*
|
||||
* @CKEditorPlugin(
|
||||
* id = "drupalmedia",
|
||||
* label = @Translation("Media Embed"),
|
||||
* )
|
||||
*
|
||||
* @internal
|
||||
* This is an internal part of the media system in Drupal core and may be
|
||||
* subject to change in minor releases. This class should not be
|
||||
* instantiated or extended by external code.
|
||||
*/
|
||||
class DrupalMedia extends PluginBase implements ContainerFactoryPluginInterface, CKEditorPluginContextualInterface, CKEditorPluginCssInterface {
|
||||
|
||||
/**
|
||||
* The module extension list.
|
||||
*
|
||||
* @var \Drupal\Core\Extension\ModuleExtensionList
|
||||
*/
|
||||
protected $moduleExtensionList;
|
||||
|
||||
/**
|
||||
* Constructs a new DrupalMedia plugin object.
|
||||
*
|
||||
* @param array $configuration
|
||||
* A configuration array containing information about the plugin instance.
|
||||
* @param string $plugin_id
|
||||
* The plugin_id for the plugin instance.
|
||||
* @param array $plugin_definition
|
||||
* The plugin implementation definition.
|
||||
* @param \Drupal\Core\Extension\ModuleExtensionList $extension_list_module
|
||||
* The module extension list.
|
||||
*/
|
||||
public function __construct(array $configuration, $plugin_id, $plugin_definition, ModuleExtensionList $extension_list_module) {
|
||||
parent::__construct($configuration, $plugin_id, $plugin_definition);
|
||||
$this->moduleExtensionList = $extension_list_module;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
|
||||
return new static(
|
||||
$configuration,
|
||||
$plugin_id,
|
||||
$plugin_definition,
|
||||
$container->get('extension.list.module')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isInternal() {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDependencies(Editor $editor) {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getLibraries(Editor $editor) {
|
||||
return [
|
||||
'core/jquery',
|
||||
'core/drupal',
|
||||
'core/drupal.ajax',
|
||||
'media/media_embed_ckeditor_theme',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getFile() {
|
||||
return $this->moduleExtensionList->getPath('media') . '/js/plugins/drupalmedia/plugin.js';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getConfig(Editor $editor) {
|
||||
return [];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function isEnabled(Editor $editor) {
|
||||
if (!$editor->hasAssociatedFilterFormat()) {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
// Automatically enable this plugin if the text format associated with this
|
||||
// text editor uses the media_embed filter.
|
||||
$filters = $editor->getFilterFormat()->filters();
|
||||
return $filters->has('media_embed') && $filters->get('media_embed')->status;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*
|
||||
* @todo Improve this in https://www.drupal.org/project/drupal/issues/3072063
|
||||
*/
|
||||
public function getCssFiles(Editor $editor) {
|
||||
return [
|
||||
$this->moduleExtensionList->getPath('media') . '/css/plugins/drupalmedia/ckeditor.drupalmedia.css',
|
||||
$this->moduleExtensionList->getPath('system') . '/css/components/hidden.module.css',
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media\Plugin\Derivative;
|
||||
|
||||
use Drupal\Component\Plugin\Derivative\DeriverBase;
|
||||
use Drupal\Core\Config\ConfigFactoryInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Plugin\Discovery\ContainerDeriverInterface;
|
||||
use Drupal\Core\StringTranslation\StringTranslationTrait;
|
||||
use Drupal\Core\StringTranslation\TranslationInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Generates media-related local tasks.
|
||||
*/
|
||||
class DynamicLocalTasks extends DeriverBase implements ContainerDeriverInterface {
|
||||
|
||||
use StringTranslationTrait;
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The media settings config.
|
||||
*
|
||||
* @var \Drupal\Core\Config\ImmutableConfig
|
||||
*/
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* Creates a DynamicLocalTasks object.
|
||||
*
|
||||
* @param \Drupal\Core\StringTranslation\TranslationInterface $string_translation
|
||||
* The translation manager.
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
|
||||
* The config factory.
|
||||
*/
|
||||
public function __construct(TranslationInterface $string_translation, EntityTypeManagerInterface $entity_type_manager, ConfigFactoryInterface $config_factory) {
|
||||
$this->stringTranslation = $string_translation;
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->config = $config_factory->get('media.settings');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, $base_plugin_id) {
|
||||
return new static(
|
||||
$container->get('string_translation'),
|
||||
$container->get('entity_type.manager'),
|
||||
$container->get('config.factory')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDerivativeDefinitions($base_plugin_definition) {
|
||||
// Provide an edit_form task if standalone media URLs are enabled.
|
||||
$this->derivatives["entity.media.canonical"] = [
|
||||
'route_name' => "entity.media.canonical",
|
||||
'title' => $this->t('Edit'),
|
||||
'base_route' => "entity.media.canonical",
|
||||
'weight' => 1,
|
||||
] + $base_plugin_definition;
|
||||
|
||||
if ($this->config->get('standalone_url')) {
|
||||
$this->derivatives["entity.media.canonical"]['title'] = $this->t('View');
|
||||
|
||||
$this->derivatives["entity.media.edit_form"] = [
|
||||
'route_name' => "entity.media.edit_form",
|
||||
'title' => $this->t('Edit'),
|
||||
'base_route' => 'entity.media.canonical',
|
||||
'weight' => 2,
|
||||
] + $base_plugin_definition;
|
||||
}
|
||||
|
||||
$this->derivatives["entity.media.delete_form"] = [
|
||||
'route_name' => "entity.media.delete_form",
|
||||
'title' => $this->t('Delete'),
|
||||
'base_route' => "entity.media.canonical",
|
||||
'weight' => 10,
|
||||
] + $base_plugin_definition;
|
||||
|
||||
return $this->derivatives;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media\Plugin\EntityReferenceSelection;
|
||||
|
||||
use Drupal\Core\Entity\Plugin\EntityReferenceSelection\DefaultSelection;
|
||||
|
||||
/**
|
||||
* Provides specific access control for the media entity type.
|
||||
*
|
||||
* @EntityReferenceSelection(
|
||||
* id = "default:media",
|
||||
* label = @Translation("Media selection"),
|
||||
* entity_types = {"media"},
|
||||
* group = "default",
|
||||
* weight = 1
|
||||
* )
|
||||
*/
|
||||
class MediaSelection extends DefaultSelection {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function buildEntityQuery($match = NULL, $match_operator = 'CONTAINS') {
|
||||
$query = parent::buildEntityQuery($match, $match_operator);
|
||||
|
||||
// Ensure that users with insufficient permission cannot see unpublished
|
||||
// entities.
|
||||
if (!$this->currentUser->hasPermission('administer media')) {
|
||||
$query->condition('status', 1);
|
||||
}
|
||||
return $query;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function createNewEntity($entity_type_id, $bundle, $label, $uid) {
|
||||
$media = parent::createNewEntity($entity_type_id, $bundle, $label, $uid);
|
||||
|
||||
// In order to create a referenceable media, it needs to published.
|
||||
/** @var \Drupal\media\MediaInterface $media */
|
||||
$media->setPublished();
|
||||
|
||||
return $media;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validateReferenceableNewEntities(array $entities) {
|
||||
$entities = parent::validateReferenceableNewEntities($entities);
|
||||
// Mirror the conditions checked in buildEntityQuery().
|
||||
if (!$this->currentUser->hasPermission('administer media')) {
|
||||
$entities = array_filter($entities, function ($media) {
|
||||
/** @var \Drupal\media\MediaInterface $media */
|
||||
return $media->isPublished();
|
||||
});
|
||||
}
|
||||
return $entities;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,201 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media\Plugin\Field\FieldFormatter;
|
||||
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Field\FieldItemListInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
use Drupal\image\ImageStyleStorageInterface;
|
||||
use Drupal\image\Plugin\Field\FieldFormatter\ImageFormatter;
|
||||
use Drupal\Core\Field\Plugin\Field\FieldType\EntityReferenceItem;
|
||||
use Drupal\Core\Render\RendererInterface;
|
||||
use Drupal\media\MediaInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Drupal\Core\Field\FieldDefinitionInterface;
|
||||
|
||||
/**
|
||||
* Plugin implementation of the 'media_thumbnail' formatter.
|
||||
*
|
||||
* @FieldFormatter(
|
||||
* id = "media_thumbnail",
|
||||
* label = @Translation("Thumbnail"),
|
||||
* field_types = {
|
||||
* "entity_reference"
|
||||
* }
|
||||
* )
|
||||
*/
|
||||
class MediaThumbnailFormatter extends ImageFormatter {
|
||||
|
||||
/**
|
||||
* The renderer service.
|
||||
*
|
||||
* @var \Drupal\Core\Render\RendererInterface
|
||||
*/
|
||||
protected $renderer;
|
||||
|
||||
/**
|
||||
* Constructs an MediaThumbnailFormatter object.
|
||||
*
|
||||
* @param string $plugin_id
|
||||
* The plugin_id for the formatter.
|
||||
* @param mixed $plugin_definition
|
||||
* The plugin implementation definition.
|
||||
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
|
||||
* The definition of the field to which the formatter is associated.
|
||||
* @param array $settings
|
||||
* The formatter settings.
|
||||
* @param string $label
|
||||
* The formatter label display setting.
|
||||
* @param string $view_mode
|
||||
* The view mode.
|
||||
* @param array $third_party_settings
|
||||
* Any third party settings settings.
|
||||
* @param \Drupal\Core\Session\AccountInterface $current_user
|
||||
* The current user.
|
||||
* @param \Drupal\image\ImageStyleStorageInterface $image_style_storage
|
||||
* The image style entity storage handler.
|
||||
* @param \Drupal\Core\Render\RendererInterface $renderer
|
||||
* The renderer service.
|
||||
*/
|
||||
public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, AccountInterface $current_user, ImageStyleStorageInterface $image_style_storage, RendererInterface $renderer) {
|
||||
parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings, $current_user, $image_style_storage);
|
||||
$this->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 item'),
|
||||
];
|
||||
$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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media\Plugin\Field\FieldFormatter;
|
||||
|
||||
use Drupal\Core\Cache\CacheableMetadata;
|
||||
use Drupal\Core\Config\ConfigFactoryInterface;
|
||||
use Drupal\Core\Field\FieldDefinitionInterface;
|
||||
use Drupal\Core\Field\FieldItemListInterface;
|
||||
use Drupal\Core\Field\FormatterBase;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
|
||||
use Drupal\Core\Messenger\MessengerInterface;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\media\Entity\MediaType;
|
||||
use Drupal\media\IFrameUrlHelper;
|
||||
use Drupal\media\OEmbed\Resource;
|
||||
use Drupal\media\OEmbed\ResourceException;
|
||||
use Drupal\media\OEmbed\ResourceFetcherInterface;
|
||||
use Drupal\media\OEmbed\UrlResolverInterface;
|
||||
use Drupal\media\Plugin\media\Source\OEmbedInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Plugin implementation of the 'oembed' formatter.
|
||||
*
|
||||
* @internal
|
||||
* This is an internal part of the oEmbed system and should only be used by
|
||||
* oEmbed-related code in Drupal core.
|
||||
*
|
||||
* @FieldFormatter(
|
||||
* id = "oembed",
|
||||
* label = @Translation("oEmbed content"),
|
||||
* field_types = {
|
||||
* "link",
|
||||
* "string",
|
||||
* "string_long",
|
||||
* },
|
||||
* )
|
||||
*/
|
||||
class OEmbedFormatter extends FormatterBase implements ContainerFactoryPluginInterface {
|
||||
|
||||
/**
|
||||
* The messenger service.
|
||||
*
|
||||
* @var \Drupal\Core\Messenger\MessengerInterface
|
||||
*/
|
||||
protected $messenger;
|
||||
|
||||
/**
|
||||
* The oEmbed resource fetcher.
|
||||
*
|
||||
* @var \Drupal\media\OEmbed\ResourceFetcherInterface
|
||||
*/
|
||||
protected $resourceFetcher;
|
||||
|
||||
/**
|
||||
* The oEmbed URL resolver service.
|
||||
*
|
||||
* @var \Drupal\media\OEmbed\UrlResolverInterface
|
||||
*/
|
||||
protected $urlResolver;
|
||||
|
||||
/**
|
||||
* The logger service.
|
||||
*
|
||||
* @var \Psr\Log\LoggerInterface
|
||||
*/
|
||||
protected $logger;
|
||||
|
||||
/**
|
||||
* The media settings config.
|
||||
*
|
||||
* @var \Drupal\Core\Config\ImmutableConfig
|
||||
*/
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* The iFrame URL helper service.
|
||||
*
|
||||
* @var \Drupal\media\IFrameUrlHelper
|
||||
*/
|
||||
protected $iFrameUrlHelper;
|
||||
|
||||
/**
|
||||
* Constructs an OEmbedFormatter instance.
|
||||
*
|
||||
* @param string $plugin_id
|
||||
* The plugin ID for the formatter.
|
||||
* @param mixed $plugin_definition
|
||||
* The plugin implementation definition.
|
||||
* @param \Drupal\Core\Field\FieldDefinitionInterface $field_definition
|
||||
* The definition of the field to which the formatter is associated.
|
||||
* @param array $settings
|
||||
* The formatter settings.
|
||||
* @param string $label
|
||||
* The formatter label display setting.
|
||||
* @param string $view_mode
|
||||
* The view mode.
|
||||
* @param array $third_party_settings
|
||||
* Any third party settings.
|
||||
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
|
||||
* The messenger service.
|
||||
* @param \Drupal\media\OEmbed\ResourceFetcherInterface $resource_fetcher
|
||||
* The oEmbed resource fetcher service.
|
||||
* @param \Drupal\media\OEmbed\UrlResolverInterface $url_resolver
|
||||
* The oEmbed URL resolver service.
|
||||
* @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
|
||||
* The logger factory service.
|
||||
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
|
||||
* The config factory service.
|
||||
* @param \Drupal\media\IFrameUrlHelper $iframe_url_helper
|
||||
* The iFrame URL helper service.
|
||||
*/
|
||||
public function __construct($plugin_id, $plugin_definition, FieldDefinitionInterface $field_definition, array $settings, $label, $view_mode, array $third_party_settings, MessengerInterface $messenger, ResourceFetcherInterface $resource_fetcher, UrlResolverInterface $url_resolver, LoggerChannelFactoryInterface $logger_factory, ConfigFactoryInterface $config_factory, IFrameUrlHelper $iframe_url_helper) {
|
||||
parent::__construct($plugin_id, $plugin_definition, $field_definition, $settings, $label, $view_mode, $third_party_settings);
|
||||
$this->messenger = $messenger;
|
||||
$this->resourceFetcher = $resource_fetcher;
|
||||
$this->urlResolver = $url_resolver;
|
||||
$this->logger = $logger_factory->get('media');
|
||||
$this->config = $config_factory->get('media.settings');
|
||||
$this->iFrameUrlHelper = $iframe_url_helper;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@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('messenger'),
|
||||
$container->get('media.oembed.resource_fetcher'),
|
||||
$container->get('media.oembed.url_resolver'),
|
||||
$container->get('logger.factory'),
|
||||
$container->get('config.factory'),
|
||||
$container->get('media.oembed.iframe_url_helper')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function defaultSettings() {
|
||||
return [
|
||||
'max_width' => 0,
|
||||
'max_height' => 0,
|
||||
] + parent::defaultSettings();
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function viewElements(FieldItemListInterface $items, $langcode) {
|
||||
$element = [];
|
||||
$max_width = $this->getSetting('max_width');
|
||||
$max_height = $this->getSetting('max_height');
|
||||
|
||||
foreach ($items as $delta => $item) {
|
||||
$main_property = $item->getFieldDefinition()->getFieldStorageDefinition()->getMainPropertyName();
|
||||
$value = $item->{$main_property};
|
||||
|
||||
if (empty($value)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
try {
|
||||
$resource_url = $this->urlResolver->getResourceUrl($value, $max_width, $max_height);
|
||||
$resource = $this->resourceFetcher->fetchResource($resource_url);
|
||||
}
|
||||
catch (ResourceException $exception) {
|
||||
$this->logger->error("Could not retrieve the remote URL (@url).", ['@url' => $value]);
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($resource->getType() === Resource::TYPE_LINK) {
|
||||
$element[$delta] = [
|
||||
'#title' => $resource->getTitle(),
|
||||
'#type' => 'link',
|
||||
'#url' => Url::fromUri($value),
|
||||
];
|
||||
}
|
||||
elseif ($resource->getType() === Resource::TYPE_PHOTO) {
|
||||
$element[$delta] = [
|
||||
'#theme' => 'image',
|
||||
'#uri' => $resource->getUrl()->toString(),
|
||||
'#width' => $max_width ?: $resource->getWidth(),
|
||||
'#height' => $max_height ?: $resource->getHeight(),
|
||||
];
|
||||
}
|
||||
else {
|
||||
$url = Url::fromRoute('media.oembed_iframe', [], [
|
||||
'query' => [
|
||||
'url' => $value,
|
||||
'max_width' => $max_width,
|
||||
'max_height' => $max_height,
|
||||
'hash' => $this->iFrameUrlHelper->getHash($value, $max_width, $max_height),
|
||||
],
|
||||
]);
|
||||
|
||||
$domain = $this->config->get('iframe_domain');
|
||||
if ($domain) {
|
||||
$url->setOption('base_url', $domain);
|
||||
}
|
||||
|
||||
// Render videos and rich content in an iframe for security reasons.
|
||||
// @see: https://oembed.com/#section3
|
||||
$element[$delta] = [
|
||||
'#type' => 'html_tag',
|
||||
'#tag' => 'iframe',
|
||||
'#attributes' => [
|
||||
'src' => $url->toString(),
|
||||
'frameborder' => 0,
|
||||
'scrolling' => FALSE,
|
||||
'allowtransparency' => TRUE,
|
||||
'width' => $max_width ?: $resource->getWidth(),
|
||||
'height' => $max_height ?: $resource->getHeight(),
|
||||
'class' => ['media-oembed-content'],
|
||||
],
|
||||
'#attached' => [
|
||||
'library' => [
|
||||
'media/oembed.formatter',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
||||
// An empty title attribute will disable title inheritance, so only
|
||||
// add it if the resource has a title.
|
||||
$title = $resource->getTitle();
|
||||
if ($title) {
|
||||
$element[$delta]['#attributes']['title'] = $title;
|
||||
}
|
||||
|
||||
CacheableMetadata::createFromObject($resource)
|
||||
->addCacheTags($this->config->getCacheTags())
|
||||
->applyTo($element[$delta]);
|
||||
}
|
||||
}
|
||||
return $element;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function settingsForm(array $form, FormStateInterface $form_state) {
|
||||
return parent::settingsForm($form, $form_state) + [
|
||||
'max_width' => [
|
||||
'#type' => 'number',
|
||||
'#title' => $this->t('Maximum width'),
|
||||
'#default_value' => $this->getSetting('max_width'),
|
||||
'#size' => 5,
|
||||
'#maxlength' => 5,
|
||||
'#field_suffix' => $this->t('pixels'),
|
||||
'#min' => 0,
|
||||
],
|
||||
'max_height' => [
|
||||
'#type' => 'number',
|
||||
'#title' => $this->t('Maximum height'),
|
||||
'#default_value' => $this->getSetting('max_height'),
|
||||
'#size' => 5,
|
||||
'#maxlength' => 5,
|
||||
'#field_suffix' => $this->t('pixels'),
|
||||
'#min' => 0,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function settingsSummary() {
|
||||
$summary = parent::settingsSummary();
|
||||
if ($this->getSetting('max_width') && $this->getSetting('max_height')) {
|
||||
$summary[] = $this->t('Maximum size: %max_width x %max_height pixels', [
|
||||
'%max_width' => $this->getSetting('max_width'),
|
||||
'%max_height' => $this->getSetting('max_height'),
|
||||
]);
|
||||
}
|
||||
elseif ($this->getSetting('max_width')) {
|
||||
$summary[] = $this->t('Maximum width: %max_width pixels', [
|
||||
'%max_width' => $this->getSetting('max_width'),
|
||||
]);
|
||||
}
|
||||
elseif ($this->getSetting('max_height')) {
|
||||
$summary[] = $this->t('Maximum height: %max_height pixels', [
|
||||
'%max_height' => $this->getSetting('max_height'),
|
||||
]);
|
||||
}
|
||||
return $summary;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function isApplicable(FieldDefinitionInterface $field_definition) {
|
||||
if ($field_definition->getTargetEntityTypeId() !== 'media') {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
if (parent::isApplicable($field_definition)) {
|
||||
$media_type = $field_definition->getTargetBundle();
|
||||
|
||||
if ($media_type) {
|
||||
$media_type = MediaType::load($media_type);
|
||||
return $media_type && $media_type->getSource() instanceof OEmbedInterface;
|
||||
}
|
||||
}
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media\Plugin\Field\FieldWidget;
|
||||
|
||||
use Drupal\Core\Field\FieldDefinitionInterface;
|
||||
use Drupal\Core\Field\FieldItemListInterface;
|
||||
use Drupal\Core\Field\Plugin\Field\FieldWidget\StringTextfieldWidget;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\media\Entity\MediaType;
|
||||
use Drupal\media\Plugin\media\Source\OEmbedInterface;
|
||||
|
||||
/**
|
||||
* Plugin implementation of the 'oembed_textfield' widget.
|
||||
*
|
||||
* @internal
|
||||
* This is an internal part of the oEmbed system and should only be used by
|
||||
* oEmbed-related code in Drupal core.
|
||||
*
|
||||
* @FieldWidget(
|
||||
* id = "oembed_textfield",
|
||||
* label = @Translation("oEmbed URL"),
|
||||
* field_types = {
|
||||
* "string",
|
||||
* },
|
||||
* )
|
||||
*/
|
||||
class OEmbedWidget extends StringTextfieldWidget {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function formElement(FieldItemListInterface $items, $delta, array $element, array &$form, FormStateInterface $form_state) {
|
||||
$element = parent::formElement($items, $delta, $element, $form, $form_state);
|
||||
|
||||
/** @var \Drupal\media\Plugin\media\Source\OEmbedInterface $source */
|
||||
$source = $items->getEntity()->getSource();
|
||||
$message = $this->t('You can link to media from the following services: @providers', ['@providers' => implode(', ', $source->getProviders())]);
|
||||
|
||||
if (!empty($element['#value']['#description'])) {
|
||||
$element['value']['#description'] = [
|
||||
'#theme' => 'item_list',
|
||||
'#items' => [$element['value']['#description'], $message],
|
||||
];
|
||||
}
|
||||
else {
|
||||
$element['value']['#description'] = $message;
|
||||
}
|
||||
|
||||
return $element;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function isApplicable(FieldDefinitionInterface $field_definition) {
|
||||
$target_bundle = $field_definition->getTargetBundle();
|
||||
|
||||
if (!parent::isApplicable($field_definition) || $field_definition->getTargetEntityTypeId() !== 'media' || !$target_bundle) {
|
||||
return FALSE;
|
||||
}
|
||||
return MediaType::load($target_bundle)->getSource() instanceof OEmbedInterface;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,535 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media\Plugin\Filter;
|
||||
|
||||
use Drupal\Component\Utility\Html;
|
||||
use Drupal\Core\Entity\EntityDisplayRepositoryInterface;
|
||||
use Drupal\Core\Entity\EntityRepositoryInterface;
|
||||
use Drupal\Core\Entity\EntityTypeBundleInfoInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Field\Plugin\Field\FieldFormatter\EntityReferenceEntityFormatter;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Drupal\Core\Render\BubbleableMetadata;
|
||||
use Drupal\Core\Render\RenderContext;
|
||||
use Drupal\Core\Render\RendererInterface;
|
||||
use Drupal\Core\Security\TrustedCallbackInterface;
|
||||
use Drupal\filter\FilterProcessResult;
|
||||
use Drupal\filter\Plugin\FilterBase;
|
||||
use Drupal\image\Plugin\Field\FieldType\ImageItem;
|
||||
use Drupal\media\MediaInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Provides a filter to embed media items using a custom tag.
|
||||
*
|
||||
* @Filter(
|
||||
* id = "media_embed",
|
||||
* title = @Translation("Embed media"),
|
||||
* description = @Translation("Embeds media items using a custom tag, <code><drupal-media></code>. If used in conjunction with the 'Align/Caption' filters, make sure this filter is configured to run after them."),
|
||||
* type = Drupal\filter\Plugin\FilterInterface::TYPE_TRANSFORM_REVERSIBLE,
|
||||
* settings = {
|
||||
* "default_view_mode" = "default",
|
||||
* "allowed_view_modes" = {},
|
||||
* "allowed_media_types" = {},
|
||||
* },
|
||||
* weight = 100,
|
||||
* )
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
class MediaEmbed extends FilterBase implements ContainerFactoryPluginInterface, TrustedCallbackInterface {
|
||||
|
||||
/**
|
||||
* The entity repository.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityRepositoryInterface
|
||||
*/
|
||||
protected $entityRepository;
|
||||
|
||||
/**
|
||||
* The entity type manager.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* The entity display repository.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface
|
||||
*/
|
||||
protected $entityDisplayRepository;
|
||||
|
||||
/**
|
||||
* The entity type bundle info service.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeBundleInfoInterface
|
||||
*/
|
||||
protected $entityTypeBundleInfo;
|
||||
|
||||
/**
|
||||
* The renderer.
|
||||
*
|
||||
* @var \Drupal\Core\Render\RendererInterface
|
||||
*/
|
||||
protected $renderer;
|
||||
|
||||
/**
|
||||
* The logger factory.
|
||||
*
|
||||
* @var \Drupal\Core\Logger\LoggerChannelFactoryInterface
|
||||
*/
|
||||
protected $loggerFactory;
|
||||
|
||||
/**
|
||||
* An array of counters for the recursive rendering protection.
|
||||
*
|
||||
* Each counter takes into account all the relevant information about the
|
||||
* field and the referenced entity that is being rendered.
|
||||
*
|
||||
* @var array
|
||||
*
|
||||
* @see \Drupal\Core\Field\Plugin\Field\FieldFormatter\EntityReferenceEntityFormatter::$recursiveRenderDepth
|
||||
*/
|
||||
protected static $recursiveRenderDepth = [];
|
||||
|
||||
/**
|
||||
* Constructs a MediaEmbed object.
|
||||
*
|
||||
* @param array $configuration
|
||||
* A configuration array containing information about the plugin instance.
|
||||
* @param string $plugin_id
|
||||
* The plugin ID for the plugin instance.
|
||||
* @param mixed $plugin_definition
|
||||
* The plugin implementation definition.
|
||||
* @param \Drupal\Core\Entity\EntityRepositoryInterface $entity_repository
|
||||
* The entity repository.
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager.
|
||||
* @param \Drupal\Core\Entity\EntityDisplayRepositoryInterface $entity_display_repository
|
||||
* The entity display repository.
|
||||
* @param \Drupal\Core\Entity\EntityTypeBundleInfoInterface $bundle_info
|
||||
* The entity type bundle info service.
|
||||
* @param \Drupal\Core\Render\RendererInterface $renderer
|
||||
* The renderer.
|
||||
* @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
|
||||
* The logger factory.
|
||||
*/
|
||||
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityRepositoryInterface $entity_repository, EntityTypeManagerInterface $entity_type_manager, EntityDisplayRepositoryInterface $entity_display_repository, EntityTypeBundleInfoInterface $bundle_info, RendererInterface $renderer, LoggerChannelFactoryInterface $logger_factory) {
|
||||
parent::__construct($configuration, $plugin_id, $plugin_definition);
|
||||
$this->entityRepository = $entity_repository;
|
||||
$this->entityTypeManager = $entity_type_manager;
|
||||
$this->entityDisplayRepository = $entity_display_repository;
|
||||
$this->entityTypeBundleInfo = $bundle_info;
|
||||
$this->renderer = $renderer;
|
||||
$this->loggerFactory = $logger_factory;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
|
||||
return new static(
|
||||
$configuration,
|
||||
$plugin_id,
|
||||
$plugin_definition,
|
||||
$container->get('entity.repository'),
|
||||
$container->get('entity_type.manager'),
|
||||
$container->get('entity_display.repository'),
|
||||
$container->get('entity_type.bundle.info'),
|
||||
$container->get('renderer'),
|
||||
$container->get('logger.factory')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function settingsForm(array $form, FormStateInterface $form_state) {
|
||||
$view_mode_options = $this->entityDisplayRepository->getViewModeOptions('media');
|
||||
|
||||
$form['default_view_mode'] = [
|
||||
'#type' => 'select',
|
||||
'#options' => $view_mode_options,
|
||||
'#title' => $this->t('Default view mode'),
|
||||
'#default_value' => $this->settings['default_view_mode'],
|
||||
'#description' => $this->t('The view mode that an embedded media item should be displayed in by default. This can be overridden using the <code>data-view-mode</code> attribute.'),
|
||||
];
|
||||
|
||||
$bundles = $this->entityTypeBundleInfo->getBundleInfo('media');
|
||||
$bundle_options = array_map(function ($item) {
|
||||
return $item['label'];
|
||||
}, $bundles);
|
||||
$form['allowed_media_types'] = [
|
||||
'#title' => $this->t('Media types selectable in the Media Library'),
|
||||
'#type' => 'checkboxes',
|
||||
'#options' => $bundle_options,
|
||||
'#default_value' => $this->settings['allowed_media_types'],
|
||||
'#description' => $this->t('If none are selected, all will be allowed.'),
|
||||
'#element_validate' => [[static::class, 'validateOptions']],
|
||||
];
|
||||
|
||||
$form['allowed_view_modes'] = [
|
||||
'#title' => $this->t("View modes selectable in the 'Edit media' dialog"),
|
||||
'#type' => 'checkboxes',
|
||||
'#options' => $view_mode_options,
|
||||
'#default_value' => $this->settings['allowed_view_modes'],
|
||||
'#description' => $this->t("If two or more view modes are selected, users will be able to update the view mode that an embedded media item should be displayed in after it has been embedded. If less than two view modes are selected, media will be embedded using the default view mode and no view mode options will appear after a media item has been embedded."),
|
||||
'#element_validate' => [[static::class, 'validateOptions']],
|
||||
];
|
||||
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* Form element validation handler.
|
||||
*
|
||||
* @param array $element
|
||||
* The allowed_view_modes form element.
|
||||
* @param \Drupal\Core\Form\FormStateInterface $form_state
|
||||
* The form state.
|
||||
*/
|
||||
public static function validateOptions(array &$element, FormStateInterface $form_state) {
|
||||
// Filters the #value property so only selected values appear in the
|
||||
// config.
|
||||
$form_state->setValueForElement($element, array_filter($element['#value']));
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the render array for the given media entity in the given langcode.
|
||||
*
|
||||
* @param \Drupal\media\MediaInterface $media
|
||||
* A media entity to render.
|
||||
* @param string $view_mode
|
||||
* The view mode to render it in.
|
||||
* @param string $langcode
|
||||
* Language code in which the media entity should be rendered.
|
||||
*
|
||||
* @return array
|
||||
* A render array.
|
||||
*/
|
||||
protected function renderMedia(MediaInterface $media, $view_mode, $langcode) {
|
||||
// Due to render caching and delayed calls, filtering happens later
|
||||
// in the rendering process through a '#pre_render' callback, so we
|
||||
// need to generate a counter for the media entity that is being embedded.
|
||||
// @see \Drupal\filter\Element\ProcessedText::preRenderText()
|
||||
$recursive_render_id = $media->uuid();
|
||||
if (isset(static::$recursiveRenderDepth[$recursive_render_id])) {
|
||||
static::$recursiveRenderDepth[$recursive_render_id]++;
|
||||
}
|
||||
else {
|
||||
static::$recursiveRenderDepth[$recursive_render_id] = 1;
|
||||
}
|
||||
// Protect ourselves from recursive rendering: return an empty render array.
|
||||
if (static::$recursiveRenderDepth[$recursive_render_id] > EntityReferenceEntityFormatter::RECURSIVE_RENDER_LIMIT) {
|
||||
$this->loggerFactory->get('media')->error('During rendering of embedded media: recursive rendering detected for %entity_id. Aborting rendering.', [
|
||||
'%entity_id' => $media->id(),
|
||||
]);
|
||||
return [];
|
||||
}
|
||||
|
||||
$build = $this->entityTypeManager
|
||||
->getViewBuilder('media')
|
||||
->view($media, $view_mode, $langcode);
|
||||
|
||||
// Allows other modules to treat embedded media items differently.
|
||||
// @see quickedit_entity_view_alter()
|
||||
$build['#embed'] = TRUE;
|
||||
|
||||
// There are a few concerns when rendering an embedded media entity:
|
||||
// - entity access checking happens not during rendering but during routing,
|
||||
// and therefore we have to do it explicitly here for the embedded entity.
|
||||
$build['#access'] = $media->access('view', NULL, TRUE);
|
||||
// - caching an embedded media entity separately is unnecessary; the host
|
||||
// entity is already render cached.
|
||||
unset($build['#cache']['keys']);
|
||||
// - Contextual Links do not make sense for embedded entities; we only allow
|
||||
// the host entity to be contextually managed.
|
||||
$build['#pre_render'][] = static::class . '::disableContextualLinks';
|
||||
// - default styling may break captioned media embeds; attach asset library
|
||||
// to ensure captions behave as intended. Do not set this at the root
|
||||
// level of the render array, otherwise it will be attached always,
|
||||
// instead of only when #access allows this media to be viewed and hence
|
||||
// only when media is actually rendered.
|
||||
$build[':media_embed']['#attached']['library'][] = 'media/filter.caption';
|
||||
|
||||
return $build;
|
||||
}
|
||||
|
||||
/**
|
||||
* Builds the render array for the indicator when media cannot be loaded.
|
||||
*
|
||||
* @return array
|
||||
* A render array.
|
||||
*/
|
||||
protected function renderMissingMediaIndicator() {
|
||||
return [
|
||||
'#theme' => 'media_embed_error',
|
||||
'#message' => $this->t('The referenced media source is missing and needs to be re-embedded.'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function process($text, $langcode) {
|
||||
$result = new FilterProcessResult($text);
|
||||
|
||||
if (stristr($text, '<drupal-media') === FALSE) {
|
||||
return $result;
|
||||
}
|
||||
|
||||
$dom = Html::load($text);
|
||||
$xpath = new \DOMXPath($dom);
|
||||
|
||||
foreach ($xpath->query('//drupal-media[@data-entity-type="media" and normalize-space(@data-entity-uuid)!=""]') as $node) {
|
||||
/** @var \DOMElement $node */
|
||||
$uuid = $node->getAttribute('data-entity-uuid');
|
||||
$view_mode_id = $node->getAttribute('data-view-mode') ?: $this->settings['default_view_mode'];
|
||||
|
||||
// Delete the consumed attributes.
|
||||
$node->removeAttribute('data-entity-type');
|
||||
$node->removeAttribute('data-entity-uuid');
|
||||
$node->removeAttribute('data-view-mode');
|
||||
|
||||
$media = $this->entityRepository->loadEntityByUuid('media', $uuid);
|
||||
assert($media === NULL || $media instanceof MediaInterface);
|
||||
if (!$media) {
|
||||
$this->loggerFactory->get('media')->error('During rendering of embedded media: the media item with UUID "@uuid" does not exist.', ['@uuid' => $uuid]);
|
||||
}
|
||||
else {
|
||||
$media = $this->entityRepository->getTranslationFromContext($media, $langcode);
|
||||
$media = clone $media;
|
||||
$this->applyPerEmbedMediaOverrides($node, $media);
|
||||
}
|
||||
|
||||
$view_mode = NULL;
|
||||
if ($view_mode_id !== EntityDisplayRepositoryInterface::DEFAULT_DISPLAY_MODE) {
|
||||
$view_mode = $this->entityRepository->loadEntityByConfigTarget('entity_view_mode', "media.$view_mode_id");
|
||||
if (!$view_mode) {
|
||||
$this->loggerFactory->get('media')->error('During rendering of embedded media: the view mode "@view-mode-id" does not exist.', ['@view-mode-id' => $view_mode_id]);
|
||||
}
|
||||
}
|
||||
|
||||
$build = $media && ($view_mode || $view_mode_id === EntityDisplayRepositoryInterface::DEFAULT_DISPLAY_MODE)
|
||||
? $this->renderMedia($media, $view_mode_id, $langcode)
|
||||
: $this->renderMissingMediaIndicator();
|
||||
|
||||
if (empty($build['#attributes']['class'])) {
|
||||
$build['#attributes']['class'] = [];
|
||||
}
|
||||
// Any attributes not consumed by the filter should be carried over to the
|
||||
// rendered embedded entity. For example, `data-align` and `data-caption`
|
||||
// should be carried over, so that even when embedded media goes missing,
|
||||
// at least the caption and visual structure won't get lost.
|
||||
foreach ($node->attributes as $attribute) {
|
||||
if ($attribute->nodeName == 'class') {
|
||||
// We don't want to overwrite the existing CSS class of the embedded
|
||||
// media (or if the media entity can't be loaded, the missing media
|
||||
// indicator). But, we need to merge in CSS classes added by other
|
||||
// filters, such as filter_align, in order for those filters to work
|
||||
// properly.
|
||||
$build['#attributes']['class'] = array_unique(array_merge($build['#attributes']['class'], explode(' ', $attribute->nodeValue)));
|
||||
}
|
||||
else {
|
||||
$build['#attributes'][$attribute->nodeName] = $attribute->nodeValue;
|
||||
}
|
||||
}
|
||||
|
||||
$this->renderIntoDomNode($build, $node, $result);
|
||||
}
|
||||
|
||||
$result->setProcessedText(Html::serialize($dom));
|
||||
|
||||
return $result;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function tips($long = FALSE) {
|
||||
if ($long) {
|
||||
return $this->t('
|
||||
<p>You can embed media items:</p>
|
||||
<ul>
|
||||
<li>Choose which media item to embed: <code><drupal-media data-entity-uuid="07bf3a2e-1941-4a44-9b02-2d1d7a41ec0e" /></code></li>
|
||||
<li>Optionally also choose a view mode: <code>data-view-mode="tiny_embed"</code>, otherwise the default view mode is used.</li>
|
||||
<li>The <code>data-entity-type="media"</code> attribute is required for consistency.</li>
|
||||
</ul>');
|
||||
}
|
||||
else {
|
||||
return $this->t('You can embed media items (using the <code><drupal-media></code> tag).');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the given render array into the given DOM node.
|
||||
*
|
||||
* @param array $build
|
||||
* The render array to render in isolation.
|
||||
* @param \DOMNode $node
|
||||
* The DOM node to render into.
|
||||
* @param \Drupal\filter\FilterProcessResult $result
|
||||
* The accumulated result of filter processing, updated with the metadata
|
||||
* bubbled during rendering.
|
||||
*/
|
||||
protected function renderIntoDomNode(array $build, \DOMNode $node, FilterProcessResult &$result) {
|
||||
// We need to render the embedded entity:
|
||||
// - without replacing placeholders, so that the placeholders are
|
||||
// only replaced at the last possible moment. Hence we cannot use
|
||||
// either renderPlain() or renderRoot(), so we must use render().
|
||||
// - without bubbling beyond this filter, because filters must
|
||||
// ensure that the bubbleable metadata for the changes they make
|
||||
// when filtering text makes it onto the FilterProcessResult
|
||||
// object that they return ($result). To prevent that bubbling, we
|
||||
// must wrap the call to render() in a render context.
|
||||
$markup = $this->renderer->executeInRenderContext(new RenderContext(), function () use (&$build) {
|
||||
return $this->renderer->render($build);
|
||||
});
|
||||
$result = $result->merge(BubbleableMetadata::createFromRenderArray($build));
|
||||
static::replaceNodeContent($node, $markup);
|
||||
}
|
||||
|
||||
/**
|
||||
* Replaces the contents of a DOMNode.
|
||||
*
|
||||
* @param \DOMNode $node
|
||||
* A DOMNode object.
|
||||
* @param string $content
|
||||
* The text or HTML that will replace the contents of $node.
|
||||
*/
|
||||
protected static function replaceNodeContent(\DOMNode &$node, $content) {
|
||||
if (strlen($content)) {
|
||||
// Load the content into a new DOMDocument and retrieve the DOM nodes.
|
||||
$replacement_nodes = Html::load($content)->getElementsByTagName('body')
|
||||
->item(0)
|
||||
->childNodes;
|
||||
}
|
||||
else {
|
||||
$replacement_nodes = [$node->ownerDocument->createTextNode('')];
|
||||
}
|
||||
|
||||
foreach ($replacement_nodes as $replacement_node) {
|
||||
// Import the replacement node from the new DOMDocument into the original
|
||||
// one, importing also the child nodes of the replacement node.
|
||||
$replacement_node = $node->ownerDocument->importNode($replacement_node, TRUE);
|
||||
$node->parentNode->insertBefore($replacement_node, $node);
|
||||
}
|
||||
$node->parentNode->removeChild($node);
|
||||
}
|
||||
|
||||
/**
|
||||
* Disables Contextual Links for the embedded media by removing its property.
|
||||
*
|
||||
* @param array $build
|
||||
* The render array for the embedded media.
|
||||
*
|
||||
* @return array
|
||||
* The updated render array.
|
||||
*
|
||||
* @see \Drupal\Core\Entity\EntityViewBuilder::addContextualLinks()
|
||||
*/
|
||||
public static function disableContextualLinks(array $build) {
|
||||
unset($build['#contextual_links']);
|
||||
return $build;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies attribute-based per-media embed overrides of media information.
|
||||
*
|
||||
* Currently, this only supports overriding an image media source's `alt` and
|
||||
* `title`. Support for more overrides may be added in the future.
|
||||
*
|
||||
* @param \DOMElement $node
|
||||
* The HTML tag whose attributes may contain overrides, and if such
|
||||
* attributes are applied, they will be considered consumed and will
|
||||
* therefore be removed from the HTML.
|
||||
* @param \Drupal\media\MediaInterface $media
|
||||
* The media entity to apply attribute-based overrides to, if any.
|
||||
*
|
||||
* @see \Drupal\media\Plugin\media\Source\Image
|
||||
*/
|
||||
protected function applyPerEmbedMediaOverrides(\DOMElement $node, MediaInterface $media) {
|
||||
if ($image_field = $this->getMediaImageSourceField($media)) {
|
||||
$settings = $media->{$image_field}->getItemDefinition()->getSettings();
|
||||
|
||||
if (!empty($settings['alt_field']) && $node->hasAttribute('alt')) {
|
||||
// Allow the display of the image without an alt tag in special cases.
|
||||
// Since setting the value in the EditorMediaDialog to an empty string
|
||||
// restores the default value, this allows special cases where the alt
|
||||
// text should not be set to the default value, but should be
|
||||
// explicitly empty instead so it can be ignored by assistive
|
||||
// technologies, such as screen readers.
|
||||
if ($node->getAttribute('alt') === '""') {
|
||||
$node->setAttribute('alt', NULL);
|
||||
}
|
||||
$media->{$image_field}->alt = $node->getAttribute('alt');
|
||||
// All media entities have a thumbnail. In the case of image media, it
|
||||
// is conceivable that a particular view mode chooses to display the
|
||||
// thumbnail instead of the image field itself since the thumbnail
|
||||
// simply shows a smaller version of the actual media. So we must update
|
||||
// its `alt` too. Because its `alt` already is inherited from the image
|
||||
// field's `alt` at entity save time.
|
||||
// @see \Drupal\media\Plugin\media\Source\Image::getMetadata()
|
||||
$media->thumbnail->alt = $node->getAttribute('alt');
|
||||
// Delete the consumed attribute.
|
||||
$node->removeAttribute('alt');
|
||||
}
|
||||
|
||||
if (!empty($settings['title_field']) && $node->hasAttribute('title')) {
|
||||
// See above, the explanations for `alt` also apply to `title`.
|
||||
$media->{$image_field}->title = $node->getAttribute('title');
|
||||
$media->thumbnail->title = $node->getAttribute('title');
|
||||
// Delete the consumed attribute.
|
||||
$node->removeAttribute('title');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get image field from source config.
|
||||
*
|
||||
* @param \Drupal\media\MediaInterface $media
|
||||
* A media entity.
|
||||
*
|
||||
* @return string|null
|
||||
* String of image field name.
|
||||
*/
|
||||
protected function getMediaImageSourceField(MediaInterface $media) {
|
||||
$field_definition = $media->getSource()
|
||||
->getSourceFieldDefinition($media->bundle->entity);
|
||||
$item_class = $field_definition->getItemDefinition()->getClass();
|
||||
if ($item_class == ImageItem::class || is_subclass_of($item_class, ImageItem::class)) {
|
||||
return $field_definition->getName();
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function trustedCallbacks() {
|
||||
return ['disableContextualLinks'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function calculateDependencies() {
|
||||
$dependencies = [];
|
||||
// Combine the view modes from both config parameters.
|
||||
$view_modes = $this->settings['allowed_view_modes'] + [$this->settings['default_view_mode']];
|
||||
$view_modes = array_unique(array_values($view_modes));
|
||||
$dependencies += ['config' => []];
|
||||
$storage = $this->entityTypeManager->getStorage('entity_view_mode');
|
||||
foreach ($view_modes as $view_mode) {
|
||||
if ($entity_view_mode = $storage->load('media.' . $view_mode)) {
|
||||
$dependencies[$entity_view_mode->getConfigDependencyKey()][] = $entity_view_mode->getConfigDependencyName();
|
||||
}
|
||||
}
|
||||
return $dependencies;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media\Plugin\QueueWorker;
|
||||
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
|
||||
use Drupal\Core\Queue\QueueWorkerBase;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Process a queue of media items to fetch their thumbnails.
|
||||
*
|
||||
* @QueueWorker(
|
||||
* id = "media_entity_thumbnail",
|
||||
* title = @Translation("Thumbnail downloader"),
|
||||
* cron = {"time" = 60}
|
||||
* )
|
||||
*/
|
||||
class ThumbnailDownloader extends QueueWorkerBase implements ContainerFactoryPluginInterface {
|
||||
|
||||
/**
|
||||
* The entity type manager service.
|
||||
*
|
||||
* @var \Drupal\Core\Entity\EntityTypeManagerInterface
|
||||
*/
|
||||
protected $entityTypeManager;
|
||||
|
||||
/**
|
||||
* Constructs a new class instance.
|
||||
*
|
||||
* @param array $configuration
|
||||
* A configuration array containing information about the plugin instance.
|
||||
* @param string $plugin_id
|
||||
* The plugin_id for the plugin instance.
|
||||
* @param mixed $plugin_definition
|
||||
* The plugin implementation definition.
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* Entity type manager service.
|
||||
*/
|
||||
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager) {
|
||||
parent::__construct($configuration, $plugin_id, $plugin_definition);
|
||||
$this->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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media\Plugin\Validation\Constraint;
|
||||
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
|
||||
/**
|
||||
* Checks if a value represents a valid oEmbed resource URL.
|
||||
*
|
||||
* @internal
|
||||
* This is an internal part of the oEmbed system and should only be used by
|
||||
* oEmbed-related code in Drupal core.
|
||||
*
|
||||
* @Constraint(
|
||||
* id = "oembed_resource",
|
||||
* label = @Translation("oEmbed resource", context = "Validation"),
|
||||
* type = {"link", "string", "string_long"}
|
||||
* )
|
||||
*/
|
||||
class OEmbedResourceConstraint extends Constraint {
|
||||
|
||||
/**
|
||||
* The error message if the URL does not match any known provider.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $unknownProviderMessage = 'The given URL does not match any known oEmbed providers.';
|
||||
|
||||
/**
|
||||
* The error message if the URL matches a disallowed provider.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $disallowedProviderMessage = 'Sorry, the @name provider is not allowed.';
|
||||
|
||||
/**
|
||||
* The error message if the URL is not a valid oEmbed resource.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $invalidResourceMessage = 'The provided URL does not represent a valid oEmbed resource.';
|
||||
|
||||
/**
|
||||
* The error message if an unexpected behavior occurs.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
public $providerErrorMessage = 'An error occurred while trying to retrieve the oEmbed provider database.';
|
||||
|
||||
}
|
||||
+149
@@ -0,0 +1,149 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media\Plugin\Validation\Constraint;
|
||||
|
||||
use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
|
||||
use Drupal\Core\Logger\LoggerChannelFactoryInterface;
|
||||
use Drupal\media\OEmbed\ProviderException;
|
||||
use Drupal\media\OEmbed\ResourceException;
|
||||
use Drupal\media\OEmbed\ResourceFetcherInterface;
|
||||
use Drupal\media\OEmbed\UrlResolverInterface;
|
||||
use Drupal\media\Plugin\media\Source\OEmbedInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
use Symfony\Component\Validator\Constraint;
|
||||
use Symfony\Component\Validator\ConstraintValidator;
|
||||
|
||||
/**
|
||||
* Validates oEmbed resource URLs.
|
||||
*
|
||||
* @internal
|
||||
* This is an internal part of the oEmbed system and should only be used by
|
||||
* oEmbed-related code in Drupal core.
|
||||
*/
|
||||
class OEmbedResourceConstraintValidator extends ConstraintValidator implements ContainerInjectionInterface {
|
||||
|
||||
/**
|
||||
* The oEmbed URL resolver service.
|
||||
*
|
||||
* @var \Drupal\media\OEmbed\UrlResolverInterface
|
||||
*/
|
||||
protected $urlResolver;
|
||||
|
||||
/**
|
||||
* The resource fetcher service.
|
||||
*
|
||||
* @var \Drupal\media\OEmbed\ResourceFetcherInterface
|
||||
*/
|
||||
protected $resourceFetcher;
|
||||
|
||||
/**
|
||||
* The logger service.
|
||||
*
|
||||
* @var \Drupal\Core\Logger\LoggerChannelInterface
|
||||
*/
|
||||
protected $logger;
|
||||
|
||||
/**
|
||||
* Constructs a new OEmbedResourceConstraintValidator.
|
||||
*
|
||||
* @param \Drupal\media\OEmbed\UrlResolverInterface $url_resolver
|
||||
* The oEmbed URL resolver service.
|
||||
* @param \Drupal\media\OEmbed\ResourceFetcherInterface $resource_fetcher
|
||||
* The resource fetcher service.
|
||||
* @param \Drupal\Core\Logger\LoggerChannelFactoryInterface $logger_factory
|
||||
* The logger service.
|
||||
*/
|
||||
public function __construct(UrlResolverInterface $url_resolver, ResourceFetcherInterface $resource_fetcher, LoggerChannelFactoryInterface $logger_factory) {
|
||||
$this->urlResolver = $url_resolver;
|
||||
$this->resourceFetcher = $resource_fetcher;
|
||||
$this->logger = $logger_factory->get('media');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function create(ContainerInterface $container) {
|
||||
return new static(
|
||||
$container->get('media.oembed.url_resolver'),
|
||||
$container->get('media.oembed.resource_fetcher'),
|
||||
$container->get('logger.factory')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validate($value, Constraint $constraint) {
|
||||
/** @var \Drupal\media\MediaInterface $media */
|
||||
$media = $value->getEntity();
|
||||
/** @var \Drupal\media\Plugin\media\Source\OEmbedInterface $source */
|
||||
$source = $media->getSource();
|
||||
|
||||
if (!($source instanceof OEmbedInterface)) {
|
||||
throw new \LogicException('Media source must implement ' . OEmbedInterface::class);
|
||||
}
|
||||
$url = $source->getSourceFieldValue($media);
|
||||
// The URL may be NULL if the source field is empty, which is invalid input.
|
||||
if (empty($url)) {
|
||||
$this->context->addViolation($constraint->invalidResourceMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure that the URL matches a provider.
|
||||
try {
|
||||
$provider = $this->urlResolver->getProviderByUrl($url);
|
||||
}
|
||||
catch (ResourceException $e) {
|
||||
$this->handleException($e, $constraint->unknownProviderMessage);
|
||||
return;
|
||||
}
|
||||
catch (ProviderException $e) {
|
||||
$this->handleException($e, $constraint->providerErrorMessage);
|
||||
return;
|
||||
}
|
||||
|
||||
// Ensure that the provider is allowed.
|
||||
if (!in_array($provider->getName(), $source->getProviders(), TRUE)) {
|
||||
$this->context->addViolation($constraint->disallowedProviderMessage, [
|
||||
'@name' => $provider->getName(),
|
||||
]);
|
||||
return;
|
||||
}
|
||||
|
||||
// Verify that resource fetching works, because some URLs might match
|
||||
// the schemes but don't support oEmbed.
|
||||
try {
|
||||
$endpoints = $provider->getEndpoints();
|
||||
$resource_url = reset($endpoints)->buildResourceUrl($url);
|
||||
$this->resourceFetcher->fetchResource($resource_url);
|
||||
}
|
||||
catch (ResourceException $e) {
|
||||
$this->handleException($e, $constraint->invalidResourceMessage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Handles exceptions that occur during validation.
|
||||
*
|
||||
* @param \Exception $e
|
||||
* The caught exception.
|
||||
* @param string $error_message
|
||||
* (optional) The error message to set as a constraint violation.
|
||||
*/
|
||||
protected function handleException(\Exception $e, $error_message = NULL) {
|
||||
if ($error_message) {
|
||||
$this->context->addViolation($error_message);
|
||||
}
|
||||
|
||||
// The oEmbed system makes heavy use of exception wrapping, so log the
|
||||
// entire exception chain to help with troubleshooting.
|
||||
do {
|
||||
// @todo If $e is a ProviderException or ResourceException, log additional
|
||||
// debugging information contained in those exceptions in
|
||||
// https://www.drupal.org/project/drupal/issues/2972846.
|
||||
$this->logger->error($e->getMessage());
|
||||
$e = $e->getPrevious();
|
||||
} while ($e);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media\Plugin\media\Source;
|
||||
|
||||
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
|
||||
use Drupal\media\MediaTypeInterface;
|
||||
|
||||
/**
|
||||
* Media source wrapping around an audio file.
|
||||
*
|
||||
* @see \Drupal\file\FileInterface
|
||||
*
|
||||
* @MediaSource(
|
||||
* id = "audio_file",
|
||||
* label = @Translation("Audio file"),
|
||||
* description = @Translation("Use audio files for reusable media."),
|
||||
* allowed_field_types = {"file"},
|
||||
* default_thumbnail_filename = "audio.png"
|
||||
* )
|
||||
*/
|
||||
class AudioFile extends File {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function createSourceField(MediaTypeInterface $type) {
|
||||
return parent::createSourceField($type)->set('settings', ['file_extensions' => 'mp3 wav aac']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function prepareViewDisplay(MediaTypeInterface $type, EntityViewDisplayInterface $display) {
|
||||
$display->setComponent($this->getSourceFieldDefinition($type)->getName(), [
|
||||
'type' => 'file_audio',
|
||||
'label' => 'visually_hidden',
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media\Plugin\media\Source;
|
||||
|
||||
use Drupal\file\FileInterface;
|
||||
use Drupal\media\MediaInterface;
|
||||
use Drupal\media\MediaTypeInterface;
|
||||
use Drupal\media\MediaSourceBase;
|
||||
|
||||
/**
|
||||
* File entity media source.
|
||||
*
|
||||
* @see \Drupal\file\FileInterface
|
||||
*
|
||||
* @MediaSource(
|
||||
* id = "file",
|
||||
* label = @Translation("File"),
|
||||
* description = @Translation("Use local files for reusable media."),
|
||||
* allowed_field_types = {"file"},
|
||||
* default_thumbnail_filename = "generic.png"
|
||||
* )
|
||||
*/
|
||||
class File extends MediaSourceBase {
|
||||
|
||||
/**
|
||||
* Key for "Name" metadata attribute.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const METADATA_ATTRIBUTE_NAME = 'name';
|
||||
|
||||
/**
|
||||
* Key for "MIME type" metadata attribute.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const METADATA_ATTRIBUTE_MIME = 'mimetype';
|
||||
|
||||
/**
|
||||
* Key for "File size" metadata attribute.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const METADATA_ATTRIBUTE_SIZE = 'filesize';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getMetadataAttributes() {
|
||||
return [
|
||||
static::METADATA_ATTRIBUTE_NAME => $this->t('Name'),
|
||||
static::METADATA_ATTRIBUTE_MIME => $this->t('MIME type'),
|
||||
static::METADATA_ATTRIBUTE_SIZE => $this->t('File size'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getMetadata(MediaInterface $media, $attribute_name) {
|
||||
/** @var \Drupal\file\FileInterface $file */
|
||||
$file = $media->get($this->configuration['source_field'])->entity;
|
||||
// If the source field is not required, it may be empty.
|
||||
if (!$file) {
|
||||
return parent::getMetadata($media, $attribute_name);
|
||||
}
|
||||
switch ($attribute_name) {
|
||||
case static::METADATA_ATTRIBUTE_NAME:
|
||||
case 'default_name':
|
||||
return $file->getFilename();
|
||||
|
||||
case static::METADATA_ATTRIBUTE_MIME:
|
||||
return $file->getMimeType();
|
||||
|
||||
case static::METADATA_ATTRIBUTE_SIZE:
|
||||
return $file->getSize();
|
||||
|
||||
case 'thumbnail_uri':
|
||||
return $this->getThumbnail($file) ?: parent::getMetadata($media, $attribute_name);
|
||||
|
||||
default:
|
||||
return parent::getMetadata($media, $attribute_name);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the thumbnail image URI based on a file entity.
|
||||
*
|
||||
* @param \Drupal\file\FileInterface $file
|
||||
* A file entity.
|
||||
*
|
||||
* @return string
|
||||
* File URI of the thumbnail image or NULL if there is no specific icon.
|
||||
*/
|
||||
protected function getThumbnail(FileInterface $file) {
|
||||
$icon_base = $this->configFactory->get('media.settings')->get('icon_base_uri');
|
||||
|
||||
// We try to automatically use the most specific icon present in the
|
||||
// $icon_base directory, based on the MIME type. For instance, if an
|
||||
// icon file named "pdf.png" is present, it will be used if the file
|
||||
// matches this MIME type.
|
||||
$mimetype = $file->getMimeType();
|
||||
$mimetype = explode('/', $mimetype);
|
||||
|
||||
$icon_names = [
|
||||
$mimetype[0] . '--' . $mimetype[1],
|
||||
$mimetype[1],
|
||||
$mimetype[0],
|
||||
];
|
||||
foreach ($icon_names as $icon_name) {
|
||||
$thumbnail = $icon_base . '/' . $icon_name . '.png';
|
||||
if (is_file($thumbnail)) {
|
||||
return $thumbnail;
|
||||
}
|
||||
}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function createSourceField(MediaTypeInterface $type) {
|
||||
return parent::createSourceField($type)->set('settings', ['file_extensions' => 'txt doc docx pdf']);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media\Plugin\media\Source;
|
||||
|
||||
use Drupal\Core\Config\ConfigFactoryInterface;
|
||||
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
|
||||
use Drupal\Core\Entity\EntityFieldManagerInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Field\FieldTypePluginManagerInterface;
|
||||
use Drupal\Core\File\FileSystemInterface;
|
||||
use Drupal\Core\Image\ImageFactory;
|
||||
use Drupal\media\MediaInterface;
|
||||
use Drupal\media\MediaTypeInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Image entity media source.
|
||||
*
|
||||
* @see \Drupal\Core\Image\ImageInterface
|
||||
*
|
||||
* @MediaSource(
|
||||
* id = "image",
|
||||
* label = @Translation("Image"),
|
||||
* description = @Translation("Use local images for reusable media."),
|
||||
* allowed_field_types = {"image"},
|
||||
* default_thumbnail_filename = "no-thumbnail.png",
|
||||
* thumbnail_alt_metadata_attribute = "thumbnail_alt_value"
|
||||
* )
|
||||
*/
|
||||
class Image extends File {
|
||||
|
||||
/**
|
||||
* Key for "image width" metadata attribute.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const METADATA_ATTRIBUTE_WIDTH = 'width';
|
||||
|
||||
/**
|
||||
* Key for "image height" metadata attribute.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const METADATA_ATTRIBUTE_HEIGHT = 'height';
|
||||
|
||||
/**
|
||||
* The image factory service.
|
||||
*
|
||||
* @var \Drupal\Core\Image\ImageFactory
|
||||
*/
|
||||
protected $imageFactory;
|
||||
|
||||
/**
|
||||
* The file system service.
|
||||
*
|
||||
* @var \Drupal\Core\File\FileSystemInterface
|
||||
*/
|
||||
protected $fileSystem;
|
||||
|
||||
/**
|
||||
* Constructs a new class instance.
|
||||
*
|
||||
* @param array $configuration
|
||||
* A configuration array containing information about the plugin instance.
|
||||
* @param string $plugin_id
|
||||
* The plugin_id for the plugin instance.
|
||||
* @param mixed $plugin_definition
|
||||
* The plugin implementation definition.
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* Entity type manager service.
|
||||
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
|
||||
* Entity field manager service.
|
||||
* @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager
|
||||
* The field type plugin manager service.
|
||||
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
|
||||
* The config factory service.
|
||||
* @param \Drupal\Core\Image\ImageFactory $image_factory
|
||||
* The image factory.
|
||||
* @param \Drupal\Core\File\FileSystemInterface $file_system
|
||||
* The file system service.
|
||||
*/
|
||||
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, FieldTypePluginManagerInterface $field_type_manager, ConfigFactoryInterface $config_factory, ImageFactory $image_factory, FileSystemInterface $file_system) {
|
||||
parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager, $entity_field_manager, $field_type_manager, $config_factory);
|
||||
|
||||
$this->imageFactory = $image_factory;
|
||||
$this->fileSystem = $file_system;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@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'),
|
||||
$container->get('image.factory'),
|
||||
$container->get('file_system')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getMetadataAttributes() {
|
||||
$attributes = parent::getMetadataAttributes();
|
||||
|
||||
$attributes += [
|
||||
static::METADATA_ATTRIBUTE_WIDTH => $this->t('Width'),
|
||||
static::METADATA_ATTRIBUTE_HEIGHT => $this->t('Height'),
|
||||
];
|
||||
|
||||
return $attributes;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getMetadata(MediaInterface $media, $name) {
|
||||
// Get the file and image data.
|
||||
/** @var \Drupal\file\FileInterface $file */
|
||||
$file = $media->get($this->configuration['source_field'])->entity;
|
||||
// If the source field is not required, it may be empty.
|
||||
if (!$file) {
|
||||
return parent::getMetadata($media, $name);
|
||||
}
|
||||
|
||||
$uri = $file->getFileUri();
|
||||
$image = $this->imageFactory->get($uri);
|
||||
switch ($name) {
|
||||
case static::METADATA_ATTRIBUTE_WIDTH:
|
||||
return $image->getWidth() ?: NULL;
|
||||
|
||||
case static::METADATA_ATTRIBUTE_HEIGHT:
|
||||
return $image->getHeight() ?: NULL;
|
||||
|
||||
case 'thumbnail_uri':
|
||||
return $uri;
|
||||
|
||||
case 'thumbnail_alt_value':
|
||||
return $media->get($this->configuration['source_field'])->alt ?: parent::getMetadata($media, $name);
|
||||
}
|
||||
|
||||
return parent::getMetadata($media, $name);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function createSourceField(MediaTypeInterface $type) {
|
||||
/** @var \Drupal\field\FieldConfigInterface $field */
|
||||
$field = parent::createSourceField($type);
|
||||
|
||||
// Reset the field to its default settings so that we don't inherit the
|
||||
// settings from the parent class' source field.
|
||||
$settings = $this->fieldTypeManager->getDefaultFieldSettings($field->getType());
|
||||
|
||||
return $field->set('settings', $settings);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function prepareViewDisplay(MediaTypeInterface $type, EntityViewDisplayInterface $display) {
|
||||
parent::prepareViewDisplay($type, $display);
|
||||
|
||||
// Use the `large` image style and do not link the image to anything.
|
||||
// This will prevent the out-of-the-box configuration from outputting very
|
||||
// large raw images. If the `large` image style has been deleted, do not
|
||||
// set an image style.
|
||||
$field_name = $this->getSourceFieldDefinition($type)->getName();
|
||||
$component = $display->getComponent($field_name);
|
||||
$component['settings']['image_link'] = '';
|
||||
$component['settings']['image_style'] = '';
|
||||
if ($this->entityTypeManager->getStorage('image_style')->load('large')) {
|
||||
$component['settings']['image_style'] = 'large';
|
||||
}
|
||||
$display->setComponent($field_name, $component);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,488 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media\Plugin\media\Source;
|
||||
|
||||
use Drupal\Component\Utility\Crypt;
|
||||
use Drupal\Core\Config\ConfigFactoryInterface;
|
||||
use Drupal\Core\Entity\Display\EntityFormDisplayInterface;
|
||||
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
|
||||
use Drupal\Core\Entity\EntityFieldManagerInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Field\FieldTypePluginManagerInterface;
|
||||
use Drupal\Core\File\Exception\FileException;
|
||||
use Drupal\Core\File\FileSystemInterface;
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\Core\Messenger\MessengerInterface;
|
||||
use Drupal\Core\Url;
|
||||
use Drupal\media\IFrameUrlHelper;
|
||||
use Drupal\media\OEmbed\Resource;
|
||||
use Drupal\media\OEmbed\ResourceException;
|
||||
use Drupal\media\MediaSourceBase;
|
||||
use Drupal\media\MediaInterface;
|
||||
use Drupal\media\MediaTypeInterface;
|
||||
use Drupal\media\OEmbed\ResourceFetcherInterface;
|
||||
use Drupal\media\OEmbed\UrlResolverInterface;
|
||||
use GuzzleHttp\ClientInterface;
|
||||
use GuzzleHttp\Exception\RequestException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Provides a media source plugin for oEmbed resources.
|
||||
*
|
||||
* For security reasons, the oEmbed source (and, therefore, anything that
|
||||
* extends it) obeys a hard-coded list of allowed third-party oEmbed providers
|
||||
* set in its plugin definition's providers array. This array is a set of
|
||||
* provider names, exactly as they appear in the canonical oEmbed provider
|
||||
* database at https://oembed.com/providers.json.
|
||||
*
|
||||
* You can implement support for additional providers by defining a new plugin
|
||||
* that uses this class. This can be done in hook_media_source_info_alter().
|
||||
* For example:
|
||||
* @code
|
||||
* <?php
|
||||
*
|
||||
* function example_media_source_info_alter(array &$sources) {
|
||||
* $sources['artwork'] = [
|
||||
* 'id' => 'artwork',
|
||||
* 'label' => t('Artwork'),
|
||||
* 'description' => t('Use artwork from Flickr and DeviantArt.'),
|
||||
* 'allowed_field_types' => ['string'],
|
||||
* 'default_thumbnail_filename' => 'no-thumbnail.png',
|
||||
* 'providers' => ['Deviantart.com', 'Flickr'],
|
||||
* 'class' => 'Drupal\media\Plugin\media\Source\OEmbed',
|
||||
* ];
|
||||
* }
|
||||
* @endcode
|
||||
* The "Deviantart.com" and "Flickr" provider names are specified in
|
||||
* https://oembed.com/providers.json. The
|
||||
* \Drupal\media\Plugin\media\Source\OEmbed class already knows how to handle
|
||||
* standard interactions with third-party oEmbed APIs, so there is no need to
|
||||
* define a new class which extends it. With the code above, you will able to
|
||||
* create media types which use the "Artwork" source plugin, and use those media
|
||||
* types to link to assets on Deviantart and Flickr.
|
||||
*
|
||||
* @MediaSource(
|
||||
* id = "oembed",
|
||||
* label = @Translation("oEmbed source"),
|
||||
* description = @Translation("Use oEmbed URL for reusable media."),
|
||||
* allowed_field_types = {"string"},
|
||||
* default_thumbnail_filename = "no-thumbnail.png",
|
||||
* deriver = "Drupal\media\Plugin\media\Source\OEmbedDeriver",
|
||||
* providers = {},
|
||||
* )
|
||||
*/
|
||||
class OEmbed extends MediaSourceBase implements OEmbedInterface {
|
||||
|
||||
/**
|
||||
* The logger channel for media.
|
||||
*
|
||||
* @var \Psr\Log\LoggerInterface
|
||||
*/
|
||||
protected $logger;
|
||||
|
||||
/**
|
||||
* The messenger service.
|
||||
*
|
||||
* @var \Drupal\Core\Messenger\MessengerInterface
|
||||
*/
|
||||
protected $messenger;
|
||||
|
||||
/**
|
||||
* The HTTP client.
|
||||
*
|
||||
* @var \GuzzleHttp\Client
|
||||
*/
|
||||
protected $httpClient;
|
||||
|
||||
/**
|
||||
* The oEmbed resource fetcher service.
|
||||
*
|
||||
* @var \Drupal\media\OEmbed\ResourceFetcherInterface
|
||||
*/
|
||||
protected $resourceFetcher;
|
||||
|
||||
/**
|
||||
* The OEmbed manager service.
|
||||
*
|
||||
* @var \Drupal\media\OEmbed\UrlResolverInterface
|
||||
*/
|
||||
protected $urlResolver;
|
||||
|
||||
/**
|
||||
* The iFrame URL helper service.
|
||||
*
|
||||
* @var \Drupal\media\IFrameUrlHelper
|
||||
*/
|
||||
protected $iFrameUrlHelper;
|
||||
|
||||
/**
|
||||
* The file system.
|
||||
*
|
||||
* @var \Drupal\Core\File\FileSystemInterface
|
||||
*/
|
||||
protected $fileSystem;
|
||||
|
||||
/**
|
||||
* Constructs a new OEmbed instance.
|
||||
*
|
||||
* @param array $configuration
|
||||
* A configuration array containing information about the plugin instance.
|
||||
* @param string $plugin_id
|
||||
* The plugin_id for the plugin instance.
|
||||
* @param mixed $plugin_definition
|
||||
* The plugin implementation definition.
|
||||
* @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
|
||||
* The entity type manager service.
|
||||
* @param \Drupal\Core\Entity\EntityFieldManagerInterface $entity_field_manager
|
||||
* The entity field manager service.
|
||||
* @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
|
||||
* The config factory service.
|
||||
* @param \Drupal\Core\Field\FieldTypePluginManagerInterface $field_type_manager
|
||||
* The field type plugin manager service.
|
||||
* @param \Psr\Log\LoggerInterface $logger
|
||||
* The logger channel for media.
|
||||
* @param \Drupal\Core\Messenger\MessengerInterface $messenger
|
||||
* The messenger service.
|
||||
* @param \GuzzleHttp\ClientInterface $http_client
|
||||
* The HTTP client.
|
||||
* @param \Drupal\media\OEmbed\ResourceFetcherInterface $resource_fetcher
|
||||
* The oEmbed resource fetcher service.
|
||||
* @param \Drupal\media\OEmbed\UrlResolverInterface $url_resolver
|
||||
* The oEmbed URL resolver service.
|
||||
* @param \Drupal\media\IFrameUrlHelper $iframe_url_helper
|
||||
* The iFrame URL helper service.
|
||||
* @param \Drupal\Core\File\FileSystemInterface $file_system
|
||||
* The file system.
|
||||
*/
|
||||
public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, ConfigFactoryInterface $config_factory, FieldTypePluginManagerInterface $field_type_manager, LoggerInterface $logger, MessengerInterface $messenger, ClientInterface $http_client, ResourceFetcherInterface $resource_fetcher, UrlResolverInterface $url_resolver, IFrameUrlHelper $iframe_url_helper, FileSystemInterface $file_system = NULL) {
|
||||
parent::__construct($configuration, $plugin_id, $plugin_definition, $entity_type_manager, $entity_field_manager, $field_type_manager, $config_factory);
|
||||
$this->logger = $logger;
|
||||
$this->messenger = $messenger;
|
||||
$this->httpClient = $http_client;
|
||||
$this->resourceFetcher = $resource_fetcher;
|
||||
$this->urlResolver = $url_resolver;
|
||||
$this->iFrameUrlHelper = $iframe_url_helper;
|
||||
if (!$file_system) {
|
||||
@trigger_error('The file_system service must be passed to OEmbed::__construct(), it is required before Drupal 9.0.0. See https://www.drupal.org/node/3006851.', E_USER_DEPRECATED);
|
||||
$file_system = \Drupal::service('file_system');
|
||||
}
|
||||
$this->fileSystem = $file_system;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@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'),
|
||||
$container->get('logger.factory')->get('media'),
|
||||
$container->get('messenger'),
|
||||
$container->get('http_client'),
|
||||
$container->get('media.oembed.resource_fetcher'),
|
||||
$container->get('media.oembed.url_resolver'),
|
||||
$container->get('media.oembed.iframe_url_helper'),
|
||||
$container->get('file_system')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getMetadataAttributes() {
|
||||
return [
|
||||
'type' => $this->t('Resource type'),
|
||||
'title' => $this->t('Resource title'),
|
||||
'author_name' => $this->t('The name of the author/owner'),
|
||||
'author_url' => $this->t('The URL of the author/owner'),
|
||||
'provider_name' => $this->t("The name of the provider"),
|
||||
'provider_url' => $this->t('The URL of the provider'),
|
||||
'cache_age' => $this->t('Suggested cache lifetime'),
|
||||
'default_name' => $this->t('Default name of the media item'),
|
||||
'thumbnail_uri' => $this->t('Local URI of the thumbnail'),
|
||||
'thumbnail_width' => $this->t('Thumbnail width'),
|
||||
'thumbnail_height' => $this->t('Thumbnail height'),
|
||||
'url' => $this->t('The source URL of the resource'),
|
||||
'width' => $this->t('The width of the resource'),
|
||||
'height' => $this->t('The height of the resource'),
|
||||
'html' => $this->t('The HTML representation of the resource'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getMetadata(MediaInterface $media, $name) {
|
||||
$media_url = $this->getSourceFieldValue($media);
|
||||
// The URL may be NULL if the source field is empty, in which case just
|
||||
// return NULL.
|
||||
if (empty($media_url)) {
|
||||
return NULL;
|
||||
}
|
||||
|
||||
try {
|
||||
$resource_url = $this->urlResolver->getResourceUrl($media_url);
|
||||
$resource = $this->resourceFetcher->fetchResource($resource_url);
|
||||
}
|
||||
catch (ResourceException $e) {
|
||||
$this->messenger->addError($e->getMessage());
|
||||
return NULL;
|
||||
}
|
||||
|
||||
switch ($name) {
|
||||
case 'default_name':
|
||||
if ($title = $this->getMetadata($media, 'title')) {
|
||||
return $title;
|
||||
}
|
||||
elseif ($url = $this->getMetadata($media, 'url')) {
|
||||
return $url;
|
||||
}
|
||||
return parent::getMetadata($media, 'default_name');
|
||||
|
||||
case 'thumbnail_uri':
|
||||
return $this->getLocalThumbnailUri($resource) ?: parent::getMetadata($media, 'thumbnail_uri');
|
||||
|
||||
case 'type':
|
||||
return $resource->getType();
|
||||
|
||||
case 'title':
|
||||
return $resource->getTitle();
|
||||
|
||||
case 'author_name':
|
||||
return $resource->getAuthorName();
|
||||
|
||||
case 'author_url':
|
||||
return $resource->getAuthorUrl();
|
||||
|
||||
case 'provider_name':
|
||||
$provider = $resource->getProvider();
|
||||
return $provider ? $provider->getName() : '';
|
||||
|
||||
case 'provider_url':
|
||||
$provider = $resource->getProvider();
|
||||
return $provider ? $provider->getUrl() : NULL;
|
||||
|
||||
case 'cache_age':
|
||||
return $resource->getCacheMaxAge();
|
||||
|
||||
case 'thumbnail_width':
|
||||
return $resource->getThumbnailWidth();
|
||||
|
||||
case 'thumbnail_height':
|
||||
return $resource->getThumbnailHeight();
|
||||
|
||||
case 'url':
|
||||
$url = $resource->getUrl();
|
||||
return $url ? $url->toString() : NULL;
|
||||
|
||||
case 'width':
|
||||
return $resource->getWidth();
|
||||
|
||||
case 'height':
|
||||
return $resource->getHeight();
|
||||
|
||||
case 'html':
|
||||
return $resource->getHtml();
|
||||
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function buildConfigurationForm(array $form, FormStateInterface $form_state) {
|
||||
$form = parent::buildConfigurationForm($form, $form_state);
|
||||
|
||||
$domain = $this->configFactory->get('media.settings')->get('iframe_domain');
|
||||
if (!$this->iFrameUrlHelper->isSecure($domain)) {
|
||||
array_unshift($form, [
|
||||
'#markup' => '<p>' . $this->t('It is potentially insecure to display oEmbed content in a frame that is served from the same domain as your main Drupal site, as this may allow execution of third-party code. <a href=":url" target="_blank">You can specify a different domain for serving oEmbed content here</a> (opens in a new window).', [
|
||||
':url' => Url::fromRoute('media.settings')->setAbsolute()->toString(),
|
||||
]) . '</p>',
|
||||
]);
|
||||
}
|
||||
|
||||
$form['thumbnails_directory'] = [
|
||||
'#type' => 'textfield',
|
||||
'#title' => $this->t('Thumbnails location'),
|
||||
'#default_value' => $this->configuration['thumbnails_directory'],
|
||||
'#description' => $this->t('Thumbnails will be fetched from the provider for local usage. This is the URI of the directory where they will be placed.'),
|
||||
'#required' => TRUE,
|
||||
];
|
||||
|
||||
$configuration = $this->getConfiguration();
|
||||
$plugin_definition = $this->getPluginDefinition();
|
||||
|
||||
$form['providers'] = [
|
||||
'#type' => 'checkboxes',
|
||||
'#title' => $this->t('Allowed providers'),
|
||||
'#default_value' => $configuration['providers'],
|
||||
'#options' => array_combine($plugin_definition['providers'], $plugin_definition['providers']),
|
||||
'#description' => $this->t('Optionally select the allowed oEmbed providers for this media type. If left blank, all providers will be allowed.'),
|
||||
];
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function submitConfigurationForm(array &$form, FormStateInterface $form_state) {
|
||||
parent::submitConfigurationForm($form, $form_state);
|
||||
$configuration = $this->getConfiguration();
|
||||
$configuration['providers'] = array_filter(array_values($configuration['providers']));
|
||||
$this->setConfiguration($configuration);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function validateConfigurationForm(array &$form, FormStateInterface $form_state) {
|
||||
$thumbnails_directory = $form_state->getValue('thumbnails_directory');
|
||||
|
||||
/** @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $stream_wrapper_manager */
|
||||
$stream_wrapper_manager = \Drupal::service('stream_wrapper_manager');
|
||||
|
||||
if (!$stream_wrapper_manager->isValidUri($thumbnails_directory)) {
|
||||
$form_state->setErrorByName('thumbnails_directory', $this->t('@path is not a valid path.', [
|
||||
'@path' => $thumbnails_directory,
|
||||
]));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function defaultConfiguration() {
|
||||
return [
|
||||
'thumbnails_directory' => 'public://oembed_thumbnails',
|
||||
'providers' => [],
|
||||
] + parent::defaultConfiguration();
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the local URI for a resource thumbnail.
|
||||
*
|
||||
* If the thumbnail is not already locally stored, this method will attempt
|
||||
* to download it.
|
||||
*
|
||||
* @param \Drupal\media\OEmbed\Resource $resource
|
||||
* The oEmbed resource.
|
||||
*
|
||||
* @return string|null
|
||||
* The local thumbnail URI, or NULL if it could not be downloaded, or if the
|
||||
* resource has no thumbnail at all.
|
||||
*
|
||||
* @todo Determine whether or not oEmbed media thumbnails should be stored
|
||||
* locally at all, and if so, whether that functionality should be
|
||||
* toggle-able. See https://www.drupal.org/project/drupal/issues/2962751 for
|
||||
* more information.
|
||||
*/
|
||||
protected function getLocalThumbnailUri(Resource $resource) {
|
||||
// If there is no remote thumbnail, there's nothing for us to fetch here.
|
||||
$remote_thumbnail_url = $resource->getThumbnailUrl();
|
||||
if (!$remote_thumbnail_url) {
|
||||
return NULL;
|
||||
}
|
||||
$remote_thumbnail_url = $remote_thumbnail_url->toString();
|
||||
|
||||
// Compute the local thumbnail URI, regardless of whether or not it exists.
|
||||
$configuration = $this->getConfiguration();
|
||||
$directory = $configuration['thumbnails_directory'];
|
||||
$local_thumbnail_uri = "$directory/" . Crypt::hashBase64($remote_thumbnail_url) . '.' . pathinfo($remote_thumbnail_url, PATHINFO_EXTENSION);
|
||||
|
||||
// If the local thumbnail already exists, return its URI.
|
||||
if (file_exists($local_thumbnail_uri)) {
|
||||
return $local_thumbnail_uri;
|
||||
}
|
||||
|
||||
// The local thumbnail doesn't exist yet, so try to download it. First,
|
||||
// ensure that the destination directory is writable, and if it's not,
|
||||
// log an error and bail out.
|
||||
if (!$this->fileSystem->prepareDirectory($directory, FileSystemInterface::CREATE_DIRECTORY | FileSystemInterface::MODIFY_PERMISSIONS)) {
|
||||
$this->logger->warning('Could not prepare thumbnail destination directory @dir for oEmbed media.', [
|
||||
'@dir' => $directory,
|
||||
]);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
try {
|
||||
$response = $this->httpClient->get($remote_thumbnail_url);
|
||||
if ($response->getStatusCode() === 200) {
|
||||
$this->fileSystem->saveData((string) $response->getBody(), $local_thumbnail_uri, FileSystemInterface::EXISTS_REPLACE);
|
||||
return $local_thumbnail_uri;
|
||||
}
|
||||
}
|
||||
catch (RequestException $e) {
|
||||
$this->logger->warning($e->getMessage());
|
||||
}
|
||||
catch (FileException $e) {
|
||||
$this->logger->warning('Could not download remote thumbnail from {url}.', [
|
||||
'url' => $remote_thumbnail_url,
|
||||
]);
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getSourceFieldConstraints() {
|
||||
return [
|
||||
'oembed_resource' => [],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function prepareViewDisplay(MediaTypeInterface $type, EntityViewDisplayInterface $display) {
|
||||
$display->setComponent($this->getSourceFieldDefinition($type)->getName(), [
|
||||
'type' => 'oembed',
|
||||
'label' => 'visually_hidden',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function prepareFormDisplay(MediaTypeInterface $type, EntityFormDisplayInterface $display) {
|
||||
parent::prepareFormDisplay($type, $display);
|
||||
$source_field = $this->getSourceFieldDefinition($type)->getName();
|
||||
|
||||
$display->setComponent($source_field, [
|
||||
'type' => 'oembed_textfield',
|
||||
'weight' => $display->getComponent($source_field)['weight'],
|
||||
]);
|
||||
$display->removeComponent('name');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getProviders() {
|
||||
$configuration = $this->getConfiguration();
|
||||
return $configuration['providers'] ?: $this->getPluginDefinition()['providers'];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function createSourceField(MediaTypeInterface $type) {
|
||||
$plugin_definition = $this->getPluginDefinition();
|
||||
|
||||
$label = (string) $this->t('@type URL', [
|
||||
'@type' => $plugin_definition['label'],
|
||||
]);
|
||||
return parent::createSourceField($type)->set('label', $label);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media\Plugin\media\Source;
|
||||
|
||||
use Drupal\Component\Plugin\Derivative\DeriverBase;
|
||||
|
||||
/**
|
||||
* Derives media source plugin definitions for supported oEmbed providers.
|
||||
*
|
||||
* @internal
|
||||
* This is an internal part of the oEmbed system and should only be used by
|
||||
* oEmbed-related code in Drupal core.
|
||||
*/
|
||||
class OEmbedDeriver extends DeriverBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getDerivativeDefinitions($base_plugin_definition) {
|
||||
$this->derivatives = [
|
||||
'video' => [
|
||||
'id' => 'video',
|
||||
'label' => t('Remote video'),
|
||||
'description' => t('Use remote video URL for reusable media.'),
|
||||
'providers' => ['YouTube', 'Vimeo'],
|
||||
'default_thumbnail_filename' => 'video.png',
|
||||
] + $base_plugin_definition,
|
||||
];
|
||||
return parent::getDerivativeDefinitions($base_plugin_definition);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media\Plugin\media\Source;
|
||||
|
||||
use Drupal\media\MediaSourceFieldConstraintsInterface;
|
||||
|
||||
/**
|
||||
* Defines additional functionality for source plugins that use oEmbed.
|
||||
*/
|
||||
interface OEmbedInterface extends MediaSourceFieldConstraintsInterface {
|
||||
|
||||
/**
|
||||
* Returns the oEmbed provider names.
|
||||
*
|
||||
* The allowed providers can be configured by the user. If it is not
|
||||
* configured, all providers supported by the plugin are returned.
|
||||
*
|
||||
* @return string[]
|
||||
* A list of oEmbed provider names.
|
||||
*/
|
||||
public function getProviders();
|
||||
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media\Plugin\media\Source;
|
||||
|
||||
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
|
||||
use Drupal\media\MediaTypeInterface;
|
||||
|
||||
/**
|
||||
* Media source wrapping around a video file.
|
||||
*
|
||||
* @see \Drupal\file\FileInterface
|
||||
*
|
||||
* @MediaSource(
|
||||
* id = "video_file",
|
||||
* label = @Translation("Video file"),
|
||||
* description = @Translation("Use video files for reusable media."),
|
||||
* allowed_field_types = {"file"},
|
||||
* default_thumbnail_filename = "video.png"
|
||||
* )
|
||||
*/
|
||||
class VideoFile extends File {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function createSourceField(MediaTypeInterface $type) {
|
||||
return parent::createSourceField($type)->set('settings', ['file_extensions' => 'mp4']);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function prepareViewDisplay(MediaTypeInterface $type, EntityViewDisplayInterface $display) {
|
||||
$display->setComponent($this->getSourceFieldDefinition($type)->getName(), [
|
||||
'type' => 'file_video',
|
||||
'label' => 'visually_hidden',
|
||||
]);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media\Plugin\views\filter;
|
||||
|
||||
use Drupal\Core\Form\FormStateInterface;
|
||||
use Drupal\views\Plugin\views\filter\FilterPluginBase;
|
||||
|
||||
/**
|
||||
* Filter by published status.
|
||||
*
|
||||
* @ingroup views_filter_handlers
|
||||
*
|
||||
* @ViewsFilter("media_status")
|
||||
*/
|
||||
class Status extends FilterPluginBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function adminSummary() {}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function operatorForm(&$form, FormStateInterface $form_state) {}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function canExpose() {
|
||||
return FALSE;
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function query() {
|
||||
$table = $this->ensureMyTable();
|
||||
$snippet = "$table.status = 1 OR ($table.uid = ***CURRENT_USER*** AND ***CURRENT_USER*** <> 0 AND ***VIEW_OWN_UNPUBLISHED_MEDIA*** = 1) OR ***ADMINISTER_MEDIA*** = 1";
|
||||
if ($this->moduleHandler->moduleExists('content_moderation')) {
|
||||
$snippet .= ' OR ***VIEW_ANY_UNPUBLISHED_NODES*** = 1';
|
||||
}
|
||||
$this->query->addWhereExpression($this->options['group'], $snippet);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function getCacheContexts() {
|
||||
$contexts = parent::getCacheContexts();
|
||||
$contexts[] = 'user';
|
||||
return $contexts;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media\Plugin\views\wizard;
|
||||
|
||||
use Drupal\views\Plugin\views\wizard\WizardPluginBase;
|
||||
|
||||
/**
|
||||
* Provides Views creation wizard for Media.
|
||||
*
|
||||
* @ViewsWizard(
|
||||
* id = "media",
|
||||
* base_table = "media_field_data",
|
||||
* title = @Translation("Media")
|
||||
* )
|
||||
*/
|
||||
class Media extends WizardPluginBase {
|
||||
|
||||
/**
|
||||
* Set the created column.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $createdColumn = 'media_field_data-created';
|
||||
|
||||
/**
|
||||
* {@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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media\Plugin\views\wizard;
|
||||
|
||||
use Drupal\views\Plugin\views\wizard\WizardPluginBase;
|
||||
|
||||
/**
|
||||
* Provides Views creation wizard for Media revisions.
|
||||
*
|
||||
* @ViewsWizard(
|
||||
* id = "media_revision",
|
||||
* base_table = "media_field_revision",
|
||||
* title = @Translation("Media revisions")
|
||||
* )
|
||||
*/
|
||||
class MediaRevision extends WizardPluginBase {
|
||||
|
||||
/**
|
||||
* Set the created column.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $createdColumn = 'media_field_revision-created';
|
||||
|
||||
/**
|
||||
* {@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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
<?php
|
||||
|
||||
namespace Drupal\media\Routing;
|
||||
|
||||
use Drupal\Core\Config\ConfigFactoryInterface;
|
||||
use Drupal\Core\Entity\EntityFieldManagerInterface;
|
||||
use Drupal\Core\Entity\EntityTypeInterface;
|
||||
use Drupal\Core\Entity\EntityTypeManagerInterface;
|
||||
use Drupal\Core\Entity\Routing\AdminHtmlRouteProvider;
|
||||
use Symfony\Component\DependencyInjection\ContainerInterface;
|
||||
|
||||
/**
|
||||
* Provides HTML routes for media pages.
|
||||
*/
|
||||
class MediaRouteProvider extends AdminHtmlRouteProvider {
|
||||
|
||||
/**
|
||||
* The media settings config.
|
||||
*
|
||||
* @var \Drupal\Core\Config\ImmutableConfig
|
||||
*/
|
||||
protected $config;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function __construct(EntityTypeManagerInterface $entity_type_manager, EntityFieldManagerInterface $entity_field_manager, ConfigFactoryInterface $config_factory) {
|
||||
parent::__construct($entity_type_manager, $entity_field_manager);
|
||||
$this->config = $config_factory->get('media.settings');
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function createInstance(ContainerInterface $container, EntityTypeInterface $entity_type) {
|
||||
return new static(
|
||||
$container->get('entity_type.manager'),
|
||||
$container->get('entity_field.manager'),
|
||||
$container->get('config.factory')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function getCanonicalRoute(EntityTypeInterface $entity_type) {
|
||||
if ($this->config->get('standalone_url')) {
|
||||
return parent::getCanonicalRoute($entity_type);
|
||||
}
|
||||
else {
|
||||
return parent::getEditFormRoute($entity_type);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{#
|
||||
/**
|
||||
* @file
|
||||
* Default theme implementation for a missing media error.
|
||||
*
|
||||
* Available variables
|
||||
* - message: The message text.
|
||||
* - attributes: HTML attributes for the containing element.
|
||||
*
|
||||
* When a response from the back end can't be returned, a related error message
|
||||
* is displayed from JavaScript.
|
||||
*
|
||||
* @see Drupal.theme.mediaEmbedPreviewError
|
||||
*
|
||||
* @ingroup themeable
|
||||
*/
|
||||
#}
|
||||
<div{{ attributes }}>
|
||||
{{ message }}
|
||||
</div>
|
||||
@@ -0,0 +1,17 @@
|
||||
{#
|
||||
/**
|
||||
* @file
|
||||
* Default theme implementation to display an oEmbed resource in an iframe.
|
||||
*
|
||||
* @ingroup themeable
|
||||
*/
|
||||
#}
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<css-placeholder token="{{ placeholder_token }}">
|
||||
</head>
|
||||
<body style="margin: 0">
|
||||
{{ media|raw }}
|
||||
</body>
|
||||
</html>
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user