Issue #2940029 by Wim Leers, krlucas, legovaer, vijaycs85, nathandentzau, phenaproxima, effulgentsia, Berdir, andrewmacpherson, tstoeckler, oknate, samuel.mortenson, slashrsm, stevector, webflo, thenchev, marcoscano, jibran, Dave Reid, cs_shadow, deepakkumar14, gngn, dpi: Add an input filter to display embedded Media entities
parent
3bb50b2b3f
commit
86736407cb
|
|
@ -0,0 +1,10 @@
|
|||
/**
|
||||
* @file
|
||||
* Caption filter: default styling for displaying Media Embed captions.
|
||||
*/
|
||||
|
||||
.caption .media .field,
|
||||
.caption .media .field * {
|
||||
float: none;
|
||||
margin: unset;
|
||||
}
|
||||
|
|
@ -23,3 +23,11 @@ oembed.frame:
|
|||
css:
|
||||
component:
|
||||
css/oembed.frame.css: {}
|
||||
|
||||
filter.caption:
|
||||
version: VERSION
|
||||
css:
|
||||
component:
|
||||
css/filter.caption.css: {}
|
||||
dependencies:
|
||||
- filter/caption
|
||||
|
|
|
|||
|
|
@ -359,3 +359,140 @@ function media_entity_type_alter(array &$entity_types) {
|
|||
$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 && $allowed_html = $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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,443 @@
|
|||
<?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\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 HTML tag. 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" = "full",
|
||||
* },
|
||||
* 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 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\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, 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->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('renderer'),
|
||||
$container->get('logger.factory')
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public function settingsForm(array $form, FormStateInterface $form_state) {
|
||||
$form['default_view_mode'] = [
|
||||
'#type' => 'select',
|
||||
'#options' => $this->entityDisplayRepository->getViewModeOptions('media'),
|
||||
'#title' => $this->t('Default view mode'),
|
||||
'#default_value' => $this->settings['default_view_mode'],
|
||||
'#description' => $this->t('The view mode that embedded media should be displayed in by default. This can be overridden by using the <code>data-view-mode</code> attribute.'),
|
||||
];
|
||||
return $form;
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 a missing media entity.
|
||||
*
|
||||
* @return array
|
||||
* A render array.
|
||||
*/
|
||||
protected function renderMissingMedia() {
|
||||
return [
|
||||
'#theme' => 'image',
|
||||
'#uri' => file_url_transform_relative(file_create_url('core/modules/media/images/icons/no-thumbnail.png')),
|
||||
'#width' => 180,
|
||||
'#height' => 180,
|
||||
'#alt' => $this->t('Missing media.'),
|
||||
'#title' => $this->t('Missing media.'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* {@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 = $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
|
||||
? $this->renderMedia($media, $view_mode_id, $langcode)
|
||||
: $this->renderMissingMedia();
|
||||
|
||||
// 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) {
|
||||
$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')) {
|
||||
$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'];
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
name: Media Filter test
|
||||
description: 'Provides functionality to test the Media Embed filter.'
|
||||
type: module
|
||||
package: Testing
|
||||
version: VERSION
|
||||
core: 8.x
|
||||
dependencies:
|
||||
- drupal:media
|
||||
|
|
@ -0,0 +1,25 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* @file
|
||||
* Helper module for the Media Embed filter tests.
|
||||
*/
|
||||
|
||||
use Drupal\Core\Access\AccessResult;
|
||||
use Drupal\Core\Entity\Display\EntityViewDisplayInterface;
|
||||
use Drupal\Core\Entity\EntityInterface;
|
||||
use Drupal\Core\Session\AccountInterface;
|
||||
|
||||
/**
|
||||
* Implements hook_entity_access().
|
||||
*/
|
||||
function media_test_filter_entity_access(EntityInterface $entity, $operation, AccountInterface $account) {
|
||||
return AccessResult::neutral()->addCacheTags(['_media_test_filter_access:' . $entity->getEntityTypeId() . ':' . $entity->id()]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Implements hook_entity_view_alter().
|
||||
*/
|
||||
function media_test_filter_entity_view_alter(&$build, EntityInterface $entity, EntityViewDisplayInterface $display) {
|
||||
$build['#attributes']['data-media-embed-test-view-mode'] = $display->getMode();
|
||||
}
|
||||
|
|
@ -0,0 +1,257 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\media\FunctionalJavascript;
|
||||
|
||||
use Drupal\filter\Entity\FilterFormat;
|
||||
|
||||
/**
|
||||
* @covers ::media_filter_format_edit_form_validate
|
||||
* @group media
|
||||
*/
|
||||
class MediaEmbedFilterConfigurationUiTest extends MediaJavascriptTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
public static function setUpBeforeClass() {
|
||||
parent::setUpBeforeClass();
|
||||
// Necessary for @covers to work.
|
||||
require_once __DIR__ . '/../../../media.module';
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$format = FilterFormat::create([
|
||||
'format' => 'media_embed_test',
|
||||
'name' => 'Test format',
|
||||
'filters' => [],
|
||||
]);
|
||||
$format->save();
|
||||
|
||||
$this->drupalLogin($this->drupalCreateUser([
|
||||
'administer filters',
|
||||
$format->getPermissionName(),
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::media_form_filter_format_add_form_alter
|
||||
* @covers ::media_filter_format_edit_form_validate
|
||||
* @dataProvider providerTestValidations
|
||||
*/
|
||||
public function testValidationWhenAdding($filter_html_status, $filter_align_status, $filter_caption_status, $filter_html_image_secure_status, $media_embed, $allowed_html, $expected_error_message) {
|
||||
$this->drupalGet('admin/config/content/formats/add');
|
||||
|
||||
// Enable the `filter_html` and `media_embed` filters.
|
||||
$page = $this->getSession()->getPage();
|
||||
$page->fillField('name', 'Another test format');
|
||||
$this->showHiddenFields();
|
||||
$page->findField('format')->setValue('another_media_embed_test');
|
||||
if ($filter_html_status) {
|
||||
$page->checkField('filters[filter_html][status]');
|
||||
}
|
||||
if ($filter_align_status) {
|
||||
$page->checkField('filters[filter_align][status]');
|
||||
}
|
||||
if ($filter_caption_status) {
|
||||
$page->checkField('filters[filter_caption][status]');
|
||||
}
|
||||
if ($filter_html_image_secure_status) {
|
||||
$page->checkField('filters[filter_html_image_secure][status]');
|
||||
}
|
||||
if ($media_embed === TRUE || is_numeric($media_embed)) {
|
||||
$page->checkField('filters[media_embed][status]');
|
||||
// Set a non-default weight.
|
||||
if (is_numeric($media_embed)) {
|
||||
$this->click('.tabledrag-toggle-weight');
|
||||
$page->selectFieldOption('filters[media_embed][weight]', $media_embed);
|
||||
}
|
||||
}
|
||||
if (!empty($allowed_html)) {
|
||||
$page->clickLink('Limit allowed HTML tags and correct faulty HTML');
|
||||
$page->fillField('filters[filter_html][settings][allowed_html]', $allowed_html);
|
||||
}
|
||||
$page->pressButton('Save configuration');
|
||||
|
||||
if ($expected_error_message) {
|
||||
$this->assertSession()->pageTextNotContains('Added text format Another test format.');
|
||||
$this->assertSession()->pageTextContains($expected_error_message);
|
||||
}
|
||||
else {
|
||||
$this->assertSession()->pageTextContains('Added text format Another test format.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::media_form_filter_format_edit_form_alter
|
||||
* @covers ::media_filter_format_edit_form_validate
|
||||
* @dataProvider providerTestValidations
|
||||
*/
|
||||
public function testValidationWhenEditing($filter_html_status, $filter_align_status, $filter_caption_status, $filter_html_image_secure_status, $media_embed, $allowed_html, $expected_error_message) {
|
||||
$this->drupalGet('admin/config/content/formats/manage/media_embed_test');
|
||||
|
||||
// Enable the `filter_html` and `media_embed` filters.
|
||||
$page = $this->getSession()->getPage();
|
||||
if ($filter_html_status) {
|
||||
$page->checkField('filters[filter_html][status]');
|
||||
}
|
||||
if ($filter_align_status) {
|
||||
$page->checkField('filters[filter_align][status]');
|
||||
}
|
||||
if ($filter_caption_status) {
|
||||
$page->checkField('filters[filter_caption][status]');
|
||||
}
|
||||
if ($filter_html_image_secure_status) {
|
||||
$page->checkField('filters[filter_html_image_secure][status]');
|
||||
}
|
||||
if ($media_embed === TRUE || is_numeric($media_embed)) {
|
||||
$page->checkField('filters[media_embed][status]');
|
||||
// Set a non-default weight.
|
||||
if (is_numeric($media_embed)) {
|
||||
$this->click('.tabledrag-toggle-weight');
|
||||
$page->selectFieldOption('filters[media_embed][weight]', $media_embed);
|
||||
}
|
||||
}
|
||||
if (!empty($allowed_html)) {
|
||||
$page->clickLink('Limit allowed HTML tags and correct faulty HTML');
|
||||
$page->fillField('filters[filter_html][settings][allowed_html]', $allowed_html);
|
||||
}
|
||||
$page->pressButton('Save configuration');
|
||||
|
||||
if ($expected_error_message) {
|
||||
$this->assertSession()->pageTextNotContains('The text format Test format has been updated.');
|
||||
$this->assertSession()->pageTextContains($expected_error_message);
|
||||
}
|
||||
else {
|
||||
$this->assertSession()->pageTextContains('The text format Test format has been updated.');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for testValidationWhenAdding() and
|
||||
* testValidationWhenEditing().
|
||||
*/
|
||||
public function providerTestValidations() {
|
||||
return [
|
||||
'Tests that no filter_html occurs when filter_html not enabled.' => [
|
||||
'filters[filter_html][status]' => FALSE,
|
||||
'filters[filter_align][status]' => FALSE,
|
||||
'filters[filter_caption][status]' => FALSE,
|
||||
'filters[filter_html_image_secure][status]' => FALSE,
|
||||
'media_embed' => TRUE,
|
||||
'allowed_html' => FALSE,
|
||||
'expected_error_message' => FALSE,
|
||||
],
|
||||
'Tests validation when both filter_html and media_embed are disabled.' => [
|
||||
'filters[filter_html][status]' => FALSE,
|
||||
'filters[filter_align][status]' => FALSE,
|
||||
'filters[filter_caption][status]' => FALSE,
|
||||
'filters[filter_html_image_secure][status]' => FALSE,
|
||||
'media_embed' => FALSE,
|
||||
'allowed_html' => FALSE,
|
||||
'expected_error_message' => FALSE,
|
||||
],
|
||||
'Tests validation when media_embed filter not enabled and filter_html is enabled.' => [
|
||||
'filters[filter_html][status]' => TRUE,
|
||||
'filters[filter_align][status]' => FALSE,
|
||||
'filters[filter_caption][status]' => FALSE,
|
||||
'filters[filter_html_image_secure][status]' => FALSE,
|
||||
'media_embed' => FALSE,
|
||||
'allowed_html' => 'default',
|
||||
'expected_error_message' => FALSE,
|
||||
],
|
||||
'Tests validation when drupal-media element has no attributes.' => [
|
||||
'filters[filter_html][status]' => TRUE,
|
||||
'filters[filter_align][status]' => FALSE,
|
||||
'filters[filter_caption][status]' => FALSE,
|
||||
'filters[filter_html_image_secure][status]' => FALSE,
|
||||
'media_embed' => TRUE,
|
||||
'allowed_html' => "<a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type='1 A I'> <li> <dl> <dt> <dd> <h2 id='jump-*'> <h3 id> <h4 id> <h5 id> <h6 id> <drupal-media>",
|
||||
'expected_error_message' => 'The <drupal-media> tag in the allowed HTML tags is missing the following attributes: data-entity-type, data-entity-uuid.',
|
||||
],
|
||||
'Tests validation when drupal-media element lacks some required attributes.' => [
|
||||
'filters[filter_html][status]' => TRUE,
|
||||
'filters[filter_align][status]' => FALSE,
|
||||
'filters[filter_caption][status]' => FALSE,
|
||||
'filters[filter_html_image_secure][status]' => FALSE,
|
||||
'media_embed' => TRUE,
|
||||
'allowed_html' => "<a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type='1 A I'> <li> <dl> <dt> <dd> <h2 id='jump-*'> <h3 id> <h4 id> <h5 id> <h6 id> <drupal-media data-entity-uuid data-align>",
|
||||
'expected_error_message' => 'The <drupal-media> tag in the allowed HTML tags is missing the following attributes: data-entity-type.',
|
||||
],
|
||||
'Tests validation when both filter_html and media_embed are enabled and configured correctly' => [
|
||||
'filters[filter_html][status]' => TRUE,
|
||||
'filters[filter_align][status]' => FALSE,
|
||||
'filters[filter_caption][status]' => FALSE,
|
||||
'filters[filter_html_image_secure][status]' => FALSE,
|
||||
'media_embed' => TRUE,
|
||||
'allowed_html' => "<a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type='1 A I'> <li> <dl> <dt> <dd> <h2 id='jump-*'> <h3 id> <h4 id> <h5 id> <h6 id> <drupal-media data-entity-type data-entity-uuid data-view-mode>",
|
||||
'expected_error_message' => FALSE,
|
||||
],
|
||||
'Order validation: media_embed before all filters' => [
|
||||
'filters[filter_html][status]' => TRUE,
|
||||
'filters[filter_align][status]' => TRUE,
|
||||
'filters[filter_caption][status]' => TRUE,
|
||||
'filters[filter_html_image_secure][status]' => TRUE,
|
||||
'media_embed' => -5,
|
||||
'allowed_html' => "<a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type='1 A I'> <li> <dl> <dt> <dd> <h2 id='jump-*'> <h3 id> <h4 id> <h5 id> <h6 id> <drupal-media data-entity-type data-entity-uuid data-view-mode>",
|
||||
'expected_error_message' => 'The Embed media filter needs to be placed after the following filters: Align images, Caption images, Restrict images to this site.',
|
||||
],
|
||||
'Order validation: media_embed before filter_align' => [
|
||||
'filters[filter_html][status]' => FALSE,
|
||||
'filters[filter_align][status]' => TRUE,
|
||||
'filters[filter_caption][status]' => FALSE,
|
||||
'filters[filter_html_image_secure][status]' => FALSE,
|
||||
'media_embed' => -5,
|
||||
'allowed_html' => '',
|
||||
'expected_error_message' => 'The Embed media filter needs to be placed after the Align images filter.',
|
||||
],
|
||||
'Order validation: media_embed before filter_caption' => [
|
||||
'filters[filter_html][status]' => FALSE,
|
||||
'filters[filter_align][status]' => FALSE,
|
||||
'filters[filter_caption][status]' => TRUE,
|
||||
'filters[filter_html_image_secure][status]' => FALSE,
|
||||
'media_embed' => -5,
|
||||
'allowed_html' => '',
|
||||
'expected_error_message' => 'The Embed media filter needs to be placed after the Caption images filter.',
|
||||
],
|
||||
'Order validation: media_embed before filter_html_image_secure' => [
|
||||
'filters[filter_html][status]' => FALSE,
|
||||
'filters[filter_align][status]' => FALSE,
|
||||
'filters[filter_caption][status]' => FALSE,
|
||||
'filters[filter_html_image_secure][status]' => TRUE,
|
||||
'media_embed' => -5,
|
||||
'allowed_html' => '',
|
||||
'expected_error_message' => 'The Embed media filter needs to be placed after the Restrict images to this site filter.',
|
||||
],
|
||||
'Order validation: media_embed after filter_align and filter_caption but before filter_html_image_secure' => [
|
||||
'filters[filter_html][status]' => TRUE,
|
||||
'filters[filter_align][status]' => TRUE,
|
||||
'filters[filter_caption][status]' => TRUE,
|
||||
'filters[filter_html_image_secure][status]' => TRUE,
|
||||
'media_embed' => 5,
|
||||
'allowed_html' => "<a href hreflang> <em> <strong> <cite> <blockquote cite> <code> <ul type> <ol start type='1 A I'> <li> <dl> <dt> <dd> <h2 id='jump-*'> <h3 id> <h4 id> <h5 id> <h6 id> <drupal-media data-entity-type data-entity-uuid data-view-mode>",
|
||||
'expected_error_message' => 'The Embed media filter needs to be placed after the Restrict images to this site filter.',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Show visually hidden fields.
|
||||
*/
|
||||
protected function showHiddenFields() {
|
||||
$script = <<<JS
|
||||
var hidden_fields = document.querySelectorAll(".visually-hidden");
|
||||
|
||||
[].forEach.call(hidden_fields, function(el) {
|
||||
el.classList.remove("visually-hidden");
|
||||
});
|
||||
JS;
|
||||
|
||||
$this->getSession()->executeScript($script);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,66 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\media\Kernel;
|
||||
|
||||
/**
|
||||
* Tests that media embed disables certain integrations.
|
||||
*
|
||||
* @coversDefaultClass \Drupal\media\Plugin\Filter\MediaEmbed
|
||||
* @group media
|
||||
*/
|
||||
class MediaEmbedFilterDisabledIntegrationsTest extends MediaEmbedFilterTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'contextual',
|
||||
'quickedit',
|
||||
// @see media_test_filter_entity_view_alter()
|
||||
'media_test_filter',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->container->get('current_user')
|
||||
->addRole($this->drupalCreateRole([
|
||||
'access contextual links',
|
||||
'access in-place editing',
|
||||
]));
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::renderMedia
|
||||
* @covers ::disableContextualLinks
|
||||
* @dataProvider providerDisabledIntegrations
|
||||
*/
|
||||
public function testDisabledIntegrations($integration_detection_selector) {
|
||||
$text = $this->createEmbedCode([
|
||||
'data-entity-type' => 'media',
|
||||
'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID,
|
||||
]);
|
||||
|
||||
$this->applyFilter($text);
|
||||
$this->assertCount(1, $this->cssSelect('div[data-media-embed-test-view-mode]'));
|
||||
$this->assertCount(0, $this->cssSelect($integration_detection_selector));
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for testDisabledIntegrations().
|
||||
*/
|
||||
public function providerDisabledIntegrations() {
|
||||
return [
|
||||
'contextual' => [
|
||||
'div[data-media-embed-test-view-mode].contextual-region',
|
||||
],
|
||||
'quickedit' => [
|
||||
'div[data-media-embed-test-view-mode][data-quickedit-entity-id]',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,452 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\media\Kernel;
|
||||
|
||||
use Drupal\Core\Cache\Cache;
|
||||
use Drupal\Core\Cache\CacheableMetadata;
|
||||
use Drupal\field\Entity\FieldConfig;
|
||||
|
||||
/**
|
||||
* @coversDefaultClass \Drupal\media\Plugin\Filter\MediaEmbed
|
||||
* @group media
|
||||
*/
|
||||
class MediaEmbedFilterTest extends MediaEmbedFilterTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
// @see media_test_filter_entity_access()
|
||||
// @see media_test_filter_entity_view_alter()
|
||||
'media_test_filter',
|
||||
];
|
||||
|
||||
/**
|
||||
* Ensures media entities are rendered correctly.
|
||||
*
|
||||
* @dataProvider providerTestBasics
|
||||
*/
|
||||
public function testBasics(array $embed_attributes, $expected_view_mode, array $expected_attributes, CacheableMetadata $expected_cacheability) {
|
||||
$content = $this->createEmbedCode($embed_attributes);
|
||||
|
||||
$result = $this->applyFilter($content);
|
||||
|
||||
$this->assertCount(1, $this->cssSelect('div[data-media-embed-test-view-mode="' . $expected_view_mode . '"]'));
|
||||
$this->assertHasAttributes($this->cssSelect('div[data-media-embed-test-view-mode="' . $expected_view_mode . '"]')[0], $expected_attributes);
|
||||
$this->assertSame($expected_cacheability->getCacheTags(), $result->getCacheTags());
|
||||
$this->assertSame($expected_cacheability->getCacheContexts(), $result->getCacheContexts());
|
||||
$this->assertSame($expected_cacheability->getCacheMaxAge(), $result->getCacheMaxAge());
|
||||
$this->assertSame(['library'], array_keys($result->getAttachments()));
|
||||
$this->assertSame(['media/filter.caption'], $result->getAttachments()['library']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for testBasics().
|
||||
*/
|
||||
public function providerTestBasics() {
|
||||
$expected_cacheability_full = (new CacheableMetadata())
|
||||
->setCacheTags([
|
||||
'_media_test_filter_access:media:1',
|
||||
'_media_test_filter_access:user:2',
|
||||
'config:image.style.thumbnail',
|
||||
'file:1',
|
||||
'media:1',
|
||||
'media_view',
|
||||
'user:2',
|
||||
])
|
||||
->setCacheContexts(['timezone', 'user.permissions'])
|
||||
->setCacheMaxAge(Cache::PERMANENT);
|
||||
|
||||
return [
|
||||
'data-entity-uuid only ⇒ default view mode "full" used' => [
|
||||
[
|
||||
'data-entity-type' => 'media',
|
||||
'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID,
|
||||
],
|
||||
'full',
|
||||
[],
|
||||
$expected_cacheability_full,
|
||||
],
|
||||
'data-entity-uuid + data-view-mode=full ⇒ specified view mode used' => [
|
||||
[
|
||||
'data-entity-type' => 'media',
|
||||
'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID,
|
||||
'data-view-mode' => 'full',
|
||||
],
|
||||
'full',
|
||||
[],
|
||||
$expected_cacheability_full,
|
||||
],
|
||||
'data-entity-uuid + data-view-mode=foobar ⇒ specified view mode used' => [
|
||||
[
|
||||
'data-entity-type' => 'media',
|
||||
'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID,
|
||||
'data-view-mode' => 'foobar',
|
||||
],
|
||||
'foobar',
|
||||
[],
|
||||
(new CacheableMetadata())
|
||||
->setCacheTags([
|
||||
'_media_test_filter_access:media:1',
|
||||
'config:image.style.medium',
|
||||
'file:1',
|
||||
'media:1',
|
||||
'media_view',
|
||||
])
|
||||
->setCacheContexts(['url.site', 'user.permissions'])
|
||||
->setCacheMaxAge(Cache::PERMANENT),
|
||||
],
|
||||
'custom attributes are retained' => [
|
||||
[
|
||||
'data-foo' => 'bar',
|
||||
'foo' => 'bar',
|
||||
'data-entity-type' => 'media',
|
||||
'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID,
|
||||
],
|
||||
'full',
|
||||
[
|
||||
'data-foo' => 'bar',
|
||||
'foo' => 'bar',
|
||||
],
|
||||
$expected_cacheability_full,
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that entity access is respected by embedding an unpublished entity.
|
||||
*
|
||||
* @dataProvider providerAccessUnpublished
|
||||
*/
|
||||
public function testAccessUnpublished($allowed_to_view_unpublished, $expected_rendered, CacheableMetadata $expected_cacheability, array $expected_attachments) {
|
||||
// Unpublish the embedded entity so we can test variations in behavior.
|
||||
$this->embeddedEntity->setUnpublished()->save();
|
||||
|
||||
// Are we testing as a user who is allowed to view the embedded entity?
|
||||
if ($allowed_to_view_unpublished) {
|
||||
$this->container->get('current_user')
|
||||
->addRole($this->drupalCreateRole(['view own unpublished media']));
|
||||
}
|
||||
|
||||
$content = $this->createEmbedCode([
|
||||
'data-entity-type' => 'media',
|
||||
'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID,
|
||||
]);
|
||||
$result = $this->applyFilter($content);
|
||||
|
||||
if (!$expected_rendered) {
|
||||
$this->assertEmpty($this->getRawContent());
|
||||
}
|
||||
else {
|
||||
$this->assertCount(1, $this->cssSelect('div[data-media-embed-test-view-mode="full"]'));
|
||||
}
|
||||
|
||||
$this->assertSame($expected_cacheability->getCacheTags(), $result->getCacheTags());
|
||||
$this->assertSame($expected_cacheability->getCacheContexts(), $result->getCacheContexts());
|
||||
$this->assertSame($expected_cacheability->getCacheMaxAge(), $result->getCacheMaxAge());
|
||||
$this->assertSame($expected_attachments, $result->getAttachments());
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for testAccessUnpublished().
|
||||
*/
|
||||
public function providerAccessUnpublished() {
|
||||
return [
|
||||
'user cannot access embedded media' => [
|
||||
FALSE,
|
||||
FALSE,
|
||||
(new CacheableMetadata())
|
||||
->setCacheTags([
|
||||
'_media_test_filter_access:media:1',
|
||||
'media:1',
|
||||
'media_view',
|
||||
])
|
||||
->setCacheContexts(['user.permissions'])
|
||||
->setCacheMaxAge(Cache::PERMANENT),
|
||||
[],
|
||||
],
|
||||
'user can access embedded media' => [
|
||||
TRUE,
|
||||
TRUE,
|
||||
(new CacheableMetadata())
|
||||
->setCacheTags([
|
||||
'_media_test_filter_access:media:1',
|
||||
'_media_test_filter_access:user:2',
|
||||
'config:image.style.thumbnail',
|
||||
'file:1',
|
||||
'media:1',
|
||||
'media_view',
|
||||
'user:2',
|
||||
])
|
||||
->setCacheContexts(['timezone', 'user', 'user.permissions'])
|
||||
->setCacheMaxAge(Cache::PERMANENT),
|
||||
['library' => ['media/filter.caption']],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers ::applyPerEmbedMediaOverrides
|
||||
* @dataProvider providerOverridesAltAndTitle
|
||||
*/
|
||||
public function testOverridesAltAndTitle($title_field_property_enabled, array $expected_title_attributes) {
|
||||
// The `alt` field property is enabled by default, the `title` one is not.
|
||||
if ($title_field_property_enabled) {
|
||||
$source_field = FieldConfig::load('media.image.field_media_image');
|
||||
$source_field->setSetting('title_field', TRUE);
|
||||
$source_field->save();
|
||||
}
|
||||
|
||||
$base = [
|
||||
'data-entity-type' => 'media',
|
||||
'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID,
|
||||
];
|
||||
$input = $this->createEmbedCode($base);
|
||||
$input .= $this->createEmbedCode([
|
||||
'alt' => 'alt 1',
|
||||
'title' => 'title 1',
|
||||
] + $base);
|
||||
$input .= $this->createEmbedCode([
|
||||
'alt' => 'alt 2',
|
||||
'title' => 'title 2',
|
||||
] + $base);
|
||||
$input .= $this->createEmbedCode([
|
||||
'alt' => 'alt 3',
|
||||
'title' => 'title 3',
|
||||
] + $base);
|
||||
|
||||
$this->applyFilter($input);
|
||||
|
||||
$img_nodes = $this->cssSelect('img');
|
||||
$this->assertCount(4, $img_nodes);
|
||||
$this->assertHasAttributes($img_nodes[0], [
|
||||
'alt' => 'default alt',
|
||||
'title' => $expected_title_attributes[0],
|
||||
]);
|
||||
$this->assertHasAttributes($img_nodes[1], [
|
||||
'alt' => 'alt 1',
|
||||
'title' => $expected_title_attributes[1],
|
||||
]);
|
||||
$this->assertHasAttributes($img_nodes[2], [
|
||||
'alt' => 'alt 2',
|
||||
'title' => $expected_title_attributes[2],
|
||||
]);
|
||||
$this->assertHasAttributes($img_nodes[3], [
|
||||
'alt' => 'alt 3',
|
||||
'title' => $expected_title_attributes[3],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for testOverridesAltAndTitle().
|
||||
*/
|
||||
public function providerOverridesAltAndTitle() {
|
||||
return [
|
||||
'`title` field property disabled ⇒ `title` is not overridable' => [
|
||||
FALSE,
|
||||
[NULL, NULL, NULL, NULL],
|
||||
],
|
||||
'`title` field property enabled ⇒ `title` is not overridable' => [
|
||||
TRUE,
|
||||
[NULL, 'title 1', 'title 2', 'title 3'],
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests the indicator for missing entities.
|
||||
*
|
||||
* @dataProvider providerMissingEntityIndicator
|
||||
*/
|
||||
public function testMissingEntityIndicator($uuid) {
|
||||
$content = $this->createEmbedCode([
|
||||
'data-entity-type' => 'media',
|
||||
'data-entity-uuid' => $uuid,
|
||||
'data-view-mode' => 'foobar',
|
||||
]);
|
||||
|
||||
// If the UUID being used in the embed is that of the sample entity, first
|
||||
// assert that it currently results in a functional embed, then delete it.
|
||||
if ($uuid === static::EMBEDDED_ENTITY_UUID) {
|
||||
$this->applyFilter($content);
|
||||
$this->assertCount(1, $this->cssSelect('div[data-media-embed-test-view-mode="foobar"]'));
|
||||
$this->embeddedEntity->delete();
|
||||
}
|
||||
|
||||
$this->applyFilter($content);
|
||||
$this->assertCount(0, $this->cssSelect('div[data-media-embed-test-view-mode="foobar"]'));
|
||||
$deleted_embed_warning = $this->cssSelect('img')[0];
|
||||
$this->assertNotEmpty($deleted_embed_warning);
|
||||
$this->assertHasAttributes($deleted_embed_warning, [
|
||||
'alt' => 'Missing media.',
|
||||
'src' => file_url_transform_relative(file_create_url('core/modules/media/images/icons/no-thumbnail.png')),
|
||||
'title' => 'Missing media.',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for testMissingEntityIndicator().
|
||||
*/
|
||||
public function providerMissingEntityIndicator() {
|
||||
return [
|
||||
'valid UUID but for a deleted entity' => [
|
||||
static::EMBEDDED_ENTITY_UUID,
|
||||
],
|
||||
'node; invalid UUID' => [
|
||||
'invalidUUID',
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that only <drupal-media> tags are processed.
|
||||
*/
|
||||
public function testOnlyDrupalMediaTagProcessed() {
|
||||
$content = $this->createEmbedCode([
|
||||
'data-entity-type' => 'media',
|
||||
'data-entity-uuid' => $this->embeddedEntity->uuid(),
|
||||
]);
|
||||
$content = str_replace('drupal-media', 'drupal-entity', $content);
|
||||
|
||||
$filter_result = $this->processText($content, 'en', ['media_embed']);
|
||||
// If input equals output, the filter didn't change anything.
|
||||
$this->assertSame($content, $filter_result->getProcessedText());
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests recursive rendering protection.
|
||||
*/
|
||||
public function testRecursionProtection() {
|
||||
$text = $this->createEmbedCode([
|
||||
'data-entity-type' => 'media',
|
||||
'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID,
|
||||
]);
|
||||
|
||||
// Render and verify the presence of the embedded entity 20 times.
|
||||
for ($i = 0; $i < 20; $i++) {
|
||||
$this->applyFilter($text);
|
||||
$this->assertCount(1, $this->cssSelect('div[data-media-embed-test-view-mode="full"]'));
|
||||
}
|
||||
|
||||
// Render a 21st time, this is exceeding the recursion limit. The entity
|
||||
// embed markup will be stripped.
|
||||
$this->applyFilter($text);
|
||||
$this->assertEmpty($this->getRawContent());
|
||||
}
|
||||
|
||||
/**
|
||||
* @covers \Drupal\filter\Plugin\Filter\FilterAlign
|
||||
* @covers \Drupal\filter\Plugin\Filter\FilterCaption
|
||||
* @dataProvider providerFilterIntegration
|
||||
*/
|
||||
public function testFilterIntegration(array $filter_ids, array $additional_attributes, $verification_selector, $expected_verification_success, array $expected_asset_libraries, $prefix = '', $suffix = '') {
|
||||
$content = $this->createEmbedCode([
|
||||
'data-entity-type' => 'media',
|
||||
'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID,
|
||||
] + $additional_attributes);
|
||||
$content = $prefix . $content . $suffix;
|
||||
|
||||
$result = $this->processText($content, 'en', $filter_ids);
|
||||
$this->setRawContent($result->getProcessedText());
|
||||
$this->assertCount($expected_verification_success ? 1 : 0, $this->cssSelect($verification_selector));
|
||||
$this->assertCount(1, $this->cssSelect('div[data-media-embed-test-view-mode="full"]'));
|
||||
$this->assertSame([
|
||||
'_media_test_filter_access:media:1',
|
||||
'_media_test_filter_access:user:2',
|
||||
'config:image.style.thumbnail',
|
||||
'file:1',
|
||||
'media:1',
|
||||
'media_view',
|
||||
'user:2',
|
||||
], $result->getCacheTags());
|
||||
$this->assertSame(['timezone', 'user.permissions'], $result->getCacheContexts());
|
||||
$this->assertSame(Cache::PERMANENT, $result->getCacheMaxAge());
|
||||
$this->assertSame(['library'], array_keys($result->getAttachments()));
|
||||
$this->assertSame($expected_asset_libraries, $result->getAttachments()['library']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for testFilterIntegration().
|
||||
*/
|
||||
public function providerFilterIntegration() {
|
||||
$default_asset_libraries = ['media/filter.caption'];
|
||||
|
||||
$caption_additional_attributes = ['data-caption' => 'Yo.'];
|
||||
$caption_verification_selector = 'figure > figcaption';
|
||||
$caption_test_cases = [
|
||||
'`data-caption`; only `media_embed` ⇒ caption absent' => [
|
||||
['media_embed'],
|
||||
$caption_additional_attributes,
|
||||
$caption_verification_selector,
|
||||
FALSE,
|
||||
$default_asset_libraries,
|
||||
],
|
||||
'`data-caption`; `filter_caption` + `media_embed` ⇒ caption present' => [
|
||||
['filter_caption', 'media_embed'],
|
||||
$caption_additional_attributes,
|
||||
$caption_verification_selector,
|
||||
TRUE,
|
||||
['filter/caption', 'media/filter.caption'],
|
||||
],
|
||||
'`<a>` + `data-caption`; `filter_caption` + `media_embed` ⇒ caption present, link preserved' => [
|
||||
['filter_caption', 'media_embed'],
|
||||
$caption_additional_attributes,
|
||||
'figure > a[href="https://www.drupal.org"] + figcaption',
|
||||
TRUE,
|
||||
['filter/caption', 'media/filter.caption'],
|
||||
'<a href="https://www.drupal.org">',
|
||||
'</a>',
|
||||
],
|
||||
];
|
||||
|
||||
$align_additional_attributes = ['data-align' => 'center'];
|
||||
$align_verification_selector = 'div[data-media-embed-test-view-mode].align-center';
|
||||
$align_test_cases = [
|
||||
'`data-align`; `media_embed` ⇒ alignment absent' => [
|
||||
['media_embed'],
|
||||
$align_additional_attributes,
|
||||
$align_verification_selector,
|
||||
FALSE,
|
||||
$default_asset_libraries,
|
||||
],
|
||||
'`data-align`; `filter_align` + `media_embed` ⇒ alignment present' => [
|
||||
['filter_align', 'media_embed'],
|
||||
$align_additional_attributes,
|
||||
$align_verification_selector,
|
||||
TRUE,
|
||||
$default_asset_libraries,
|
||||
],
|
||||
'`<a>` + `data-align`; `filter_align` + `media_embed` ⇒ alignment present, link preserved' => [
|
||||
['filter_align', 'media_embed'],
|
||||
$align_additional_attributes,
|
||||
'a[href="https://www.drupal.org"] > div[data-media-embed-test-view-mode].align-center',
|
||||
TRUE,
|
||||
$default_asset_libraries,
|
||||
'<a href="https://www.drupal.org">',
|
||||
'</a>',
|
||||
],
|
||||
];
|
||||
|
||||
$caption_and_align_test_cases = [
|
||||
'`data-caption` + `data-align`; `filter_align` + `filter_caption` + `media_embed` ⇒ aligned caption present' => [
|
||||
['filter_align', 'filter_caption', 'media_embed'],
|
||||
$align_additional_attributes + $caption_additional_attributes,
|
||||
'figure.align-center > figcaption',
|
||||
TRUE,
|
||||
['filter/caption', 'media/filter.caption'],
|
||||
],
|
||||
'`<a>` + `data-caption` + `data-align`; `filter_align` + `filter_caption` + `media_embed` ⇒ aligned caption present, link preserved' => [
|
||||
['filter_align', 'filter_caption', 'media_embed'],
|
||||
$align_additional_attributes + $caption_additional_attributes,
|
||||
'figure.align-center > a[href="https://www.drupal.org"] + figcaption',
|
||||
TRUE,
|
||||
['filter/caption', 'media/filter.caption'],
|
||||
'<a href="https://www.drupal.org">',
|
||||
'</a>',
|
||||
],
|
||||
];
|
||||
|
||||
return $caption_test_cases + $align_test_cases + $caption_and_align_test_cases;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,253 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\media\Kernel;
|
||||
|
||||
use Drupal\Component\Utility\Html;
|
||||
use Drupal\Core\Entity\Entity\EntityViewDisplay;
|
||||
use Drupal\Core\Entity\Entity\EntityViewMode;
|
||||
use Drupal\Core\Language\LanguageInterface;
|
||||
use Drupal\Core\Render\BubbleableMetadata;
|
||||
use Drupal\Core\Render\RenderContext;
|
||||
use Drupal\file\Entity\File;
|
||||
use Drupal\filter\FilterPluginCollection;
|
||||
use Drupal\filter\FilterProcessResult;
|
||||
use Drupal\KernelTests\KernelTestBase;
|
||||
use Drupal\media\Entity\Media;
|
||||
use Drupal\Tests\media\Traits\MediaTypeCreationTrait;
|
||||
use Drupal\Tests\TestFileCreationTrait;
|
||||
use Drupal\Tests\user\Traits\UserCreationTrait;
|
||||
|
||||
/**
|
||||
* Base class for Media Embed filter tests.
|
||||
*/
|
||||
abstract class MediaEmbedFilterTestBase extends KernelTestBase {
|
||||
|
||||
use MediaTypeCreationTrait;
|
||||
use TestFileCreationTrait;
|
||||
use UserCreationTrait {
|
||||
createUser as drupalCreateUser;
|
||||
createRole as drupalCreateRole;
|
||||
}
|
||||
|
||||
/**
|
||||
* The UUID to use for the embedded entity.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
const EMBEDDED_ENTITY_UUID = 'e7a3e1fe-b69b-417e-8ee4-c80cb7640e63';
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'field',
|
||||
'file',
|
||||
'filter',
|
||||
'image',
|
||||
'media',
|
||||
'system',
|
||||
'text',
|
||||
'user',
|
||||
];
|
||||
|
||||
/**
|
||||
* The image file to use in tests.
|
||||
*
|
||||
* @var \Drupal\file\FileInterface
|
||||
*/
|
||||
protected $image;
|
||||
|
||||
/**
|
||||
* The sample Media entity to embed.
|
||||
*
|
||||
* @var \Drupal\media\MediaInterface
|
||||
*/
|
||||
protected $embeddedEntity;
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
$this->installSchema('file', ['file_usage']);
|
||||
$this->installSchema('system', 'sequences');
|
||||
$this->installEntitySchema('file');
|
||||
$this->installEntitySchema('media');
|
||||
$this->installEntitySchema('user');
|
||||
$this->installConfig('filter');
|
||||
$this->installConfig('image');
|
||||
$this->installConfig('media');
|
||||
$this->installConfig('system');
|
||||
|
||||
// Create a user with required permissions. Ensure that we don't use user 1
|
||||
// because that user is treated in special ways by access control handlers.
|
||||
$admin_user = $this->drupalCreateUser([]);
|
||||
$user = $this->drupalCreateUser([
|
||||
'access content',
|
||||
'view media',
|
||||
]);
|
||||
$this->container->set('current_user', $user);
|
||||
|
||||
$this->image = File::create([
|
||||
'uri' => $this->getTestFiles('image')[0]->uri,
|
||||
'uid' => 2,
|
||||
]);
|
||||
$this->image->setPermanent();
|
||||
$this->image->save();
|
||||
|
||||
// Create a sample media entity to be embedded.
|
||||
$media_type = $this->createMediaType('image', ['id' => 'image']);
|
||||
EntityViewMode::create([
|
||||
'id' => 'media.foobar',
|
||||
'targetEntityType' => 'media',
|
||||
'status' => TRUE,
|
||||
'enabled' => TRUE,
|
||||
'label' => $this->randomMachineName(),
|
||||
])->save();
|
||||
EntityViewDisplay::create([
|
||||
'targetEntityType' => 'media',
|
||||
'bundle' => $media_type->id(),
|
||||
'mode' => 'foobar',
|
||||
'status' => TRUE,
|
||||
])->removeComponent('thumbnail')
|
||||
->removeComponent('created')
|
||||
->removeComponent('uid')
|
||||
->setComponent('field_media_image', [
|
||||
'label' => 'visually_hidden',
|
||||
'type' => 'image',
|
||||
'settings' => [
|
||||
'image_style' => 'medium',
|
||||
'image_link' => 'file',
|
||||
],
|
||||
'third_party_settings' => [],
|
||||
'weight' => 1,
|
||||
'region' => 'content',
|
||||
])
|
||||
->save();
|
||||
$media = Media::create([
|
||||
'uuid' => static::EMBEDDED_ENTITY_UUID,
|
||||
'bundle' => 'image',
|
||||
'name' => 'Screaming hairy armadillo',
|
||||
'field_media_image' => [
|
||||
[
|
||||
'target_id' => $this->image->id(),
|
||||
'alt' => 'default alt',
|
||||
'title' => 'default title',
|
||||
],
|
||||
],
|
||||
])->setOwner($user);
|
||||
$media->save();
|
||||
$this->embeddedEntity = $media;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets an embed code with given attributes.
|
||||
*
|
||||
* @param array $attributes
|
||||
* The attributes to add.
|
||||
*
|
||||
* @return string
|
||||
* A string containing a drupal-entity dom element.
|
||||
*
|
||||
* @see assertEntityEmbedFilterHasRun()
|
||||
*/
|
||||
protected function createEmbedCode(array $attributes) {
|
||||
$dom = Html::load('<drupal-media>This placeholder should not be rendered.</drupal-media>');
|
||||
$xpath = new \DOMXPath($dom);
|
||||
$drupal_entity = $xpath->query('//drupal-media')[0];
|
||||
foreach ($attributes as $attribute => $value) {
|
||||
$drupal_entity->setAttribute($attribute, $value);
|
||||
}
|
||||
return Html::serialize($dom);
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies the `@Filter=media_embed` filter to text, pipes to raw content.
|
||||
*
|
||||
* @param string $text
|
||||
* The text string to be filtered.
|
||||
* @param string $langcode
|
||||
* The language code of the text to be filtered.
|
||||
*
|
||||
* @return \Drupal\filter\FilterProcessResult
|
||||
* The filtered text, wrapped in a FilterProcessResult object, and possibly
|
||||
* with associated assets, cacheability metadata and placeholders.
|
||||
*
|
||||
* @see \Drupal\Tests\entity_embed\Kernel\EntityEmbedFilterTestBase::createEmbedCode()
|
||||
* @see \Drupal\KernelTests\AssertContentTrait::setRawContent()
|
||||
*/
|
||||
protected function applyFilter($text, $langcode = 'en') {
|
||||
$this->assertContains('<drupal-media', $text);
|
||||
$this->assertContains('This placeholder should not be rendered.', $text);
|
||||
$filter_result = $this->processText($text, $langcode);
|
||||
$output = $filter_result->getProcessedText();
|
||||
$this->assertNotContains('<drupal-media', $output);
|
||||
$this->assertNotContains('This placeholder should not be rendered.', $output);
|
||||
$this->setRawContent($output);
|
||||
return $filter_result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Assert that the SimpleXMLElement object has the given attributes.
|
||||
*
|
||||
* @param \SimpleXMLElement $element
|
||||
* The SimpleXMLElement object to check.
|
||||
* @param array $expected_attributes
|
||||
* An array of expected attributes.
|
||||
*/
|
||||
protected function assertHasAttributes(\SimpleXMLElement $element, array $expected_attributes) {
|
||||
foreach ($expected_attributes as $attribute => $value) {
|
||||
if ($value === NULL) {
|
||||
$this->assertNull($element[$attribute]);
|
||||
}
|
||||
else {
|
||||
$this->assertSame((string) $value, (string) $element[$attribute]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Processes text through the provided filters.
|
||||
*
|
||||
* @param string $text
|
||||
* The text string to be filtered.
|
||||
* @param string $langcode
|
||||
* The language code of the text to be filtered.
|
||||
* @param string[] $filter_ids
|
||||
* (optional) The filter plugin IDs to apply to the given text, in the order
|
||||
* they are being requested to be executed.
|
||||
*
|
||||
* @return \Drupal\filter\FilterProcessResult
|
||||
* The filtered text, wrapped in a FilterProcessResult object, and possibly
|
||||
* with associated assets, cacheability metadata and placeholders.
|
||||
*
|
||||
* @see \Drupal\filter\Element\ProcessedText::preRenderText()
|
||||
*/
|
||||
protected function processText($text, $langcode = LanguageInterface::LANGCODE_NOT_SPECIFIED, array $filter_ids = ['media_embed']) {
|
||||
$manager = $this->container->get('plugin.manager.filter');
|
||||
$bag = new FilterPluginCollection($manager, []);
|
||||
$filters = [];
|
||||
foreach ($filter_ids as $filter_id) {
|
||||
$filters[] = $bag->get($filter_id);
|
||||
}
|
||||
|
||||
$render_context = new RenderContext();
|
||||
/** @var \Drupal\filter\FilterProcessResult $filter_result */
|
||||
$filter_result = $this->container->get('renderer')->executeInRenderContext($render_context, function () use ($text, $filters, $langcode) {
|
||||
$metadata = new BubbleableMetadata();
|
||||
foreach ($filters as $filter) {
|
||||
/** @var \Drupal\filter\FilterProcessResult $result */
|
||||
$result = $filter->process($text, $langcode);
|
||||
$metadata = $metadata->merge($result);
|
||||
$text = $result->getProcessedText();
|
||||
}
|
||||
return (new FilterProcessResult($text))->merge($metadata);
|
||||
});
|
||||
if (!$render_context->isEmpty()) {
|
||||
$filter_result = $filter_result->merge($render_context->pop());
|
||||
}
|
||||
return $filter_result;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
<?php
|
||||
|
||||
namespace Drupal\Tests\media\Kernel;
|
||||
|
||||
use Drupal\language\Entity\ConfigurableLanguage;
|
||||
|
||||
/**
|
||||
* Tests that media embeds are translated based on text (host entity) language.
|
||||
*
|
||||
* @coversDefaultClass \Drupal\media\Plugin\Filter\MediaEmbed
|
||||
* @group media
|
||||
*/
|
||||
class MediaEmbedFilterTranslationTest extends MediaEmbedFilterTestBase {
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected static $modules = [
|
||||
'language',
|
||||
];
|
||||
|
||||
/**
|
||||
* {@inheritdoc}
|
||||
*/
|
||||
protected function setUp() {
|
||||
parent::setUp();
|
||||
|
||||
ConfigurableLanguage::createFromLangcode('pt-br')->save();
|
||||
// Reload the entity to ensure it is aware of the newly created language.
|
||||
$this->embeddedEntity = $this->container->get('entity_type.manager')
|
||||
->getStorage('media')
|
||||
->load($this->embeddedEntity->id());
|
||||
|
||||
$this->embeddedEntity->addTranslation('pt-br')
|
||||
->set('field_media_image', [
|
||||
'target_id' => $this->image->id(),
|
||||
'alt' => 'pt-br alt',
|
||||
'title' => 'pt-br title',
|
||||
])->save();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tests that the expected embedded media entity translation is selected.
|
||||
*
|
||||
* @dataProvider providerTranslationSituations
|
||||
*/
|
||||
public function testTranslationSelection($text_langcode, $expected_title_langcode) {
|
||||
$text = $this->createEmbedCode([
|
||||
'data-entity-type' => 'media',
|
||||
'data-entity-uuid' => static::EMBEDDED_ENTITY_UUID,
|
||||
]);
|
||||
|
||||
$result = $this->processText($text, $text_langcode, ['media_embed']);
|
||||
$this->setRawContent($result->getProcessedText());
|
||||
|
||||
$this->assertSame(
|
||||
$this->embeddedEntity->getTranslation($expected_title_langcode)->field_media_image->alt,
|
||||
(string) $this->cssSelect('img')[0]->attributes()['alt']
|
||||
);
|
||||
// Verify that the filtered text does not vary by translation-related cache
|
||||
// contexts: a particular translation of the embedded entity is selected
|
||||
// based on the host entity's language, which should require a cache context
|
||||
// to be associated. (The host entity's language may itself be selected
|
||||
// based on the request context, but that is of no concern to this filter.)
|
||||
$this->assertSame($result->getCacheContexts(), ['timezone', 'user.permissions']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Data provider for testTranslationSelection().
|
||||
*/
|
||||
public function providerTranslationSituations() {
|
||||
$embedded_entity_translation_languages = ['en', 'pt-br'];
|
||||
|
||||
foreach (['en', 'pt-br', 'nl'] as $text_langcode) {
|
||||
// The text language (which is set to the host entity's language) must be
|
||||
// respected in selecting a translation. If that translation does not
|
||||
// exist, it falls back to the default translation of the embedded entity.
|
||||
$match_or_fallback_langcode = in_array($text_langcode, $embedded_entity_translation_languages)
|
||||
? $text_langcode
|
||||
: 'en';
|
||||
yield "text_langcode=$text_langcode ⇒ $match_or_fallback_langcode" => [
|
||||
$text_langcode,
|
||||
$match_or_fallback_langcode,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -158,6 +158,10 @@ function quickedit_preprocess_field(&$variables) {
|
|||
* Implements hook_entity_view_alter().
|
||||
*/
|
||||
function quickedit_entity_view_alter(&$build, EntityInterface $entity, EntityViewDisplayInterface $display) {
|
||||
if (isset($build['#embed'])) {
|
||||
return;
|
||||
}
|
||||
|
||||
$build['#cache']['contexts'][] = 'user.permissions';
|
||||
if (!\Drupal::currentUser()->hasPermission('access in-place editing') || ($entity instanceof RevisionableInterface && !$entity->isLatestRevision())) {
|
||||
return;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,10 @@
|
|||
/**
|
||||
* @file
|
||||
* Caption filter: default styling for displaying Media Embed captions.
|
||||
*/
|
||||
|
||||
.caption .media .field,
|
||||
.caption .media .field * {
|
||||
float: none;
|
||||
margin: unset;
|
||||
}
|
||||
|
|
@ -144,6 +144,11 @@ libraries-override:
|
|||
component:
|
||||
css/locale.admin.css: css/locale/locale.admin.css
|
||||
|
||||
media/filter.caption:
|
||||
css:
|
||||
component:
|
||||
css/filter.caption.css: css/media/filter.caption.css
|
||||
|
||||
media/oembed.formatter:
|
||||
css:
|
||||
component:
|
||||
|
|
|
|||
Loading…
Reference in New Issue