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

merge-requests/1119/head
effulgentsia 2019-07-10 23:23:07 -07:00
parent 3bb50b2b3f
commit 86736407cb
14 changed files with 1766 additions and 0 deletions

View File

@ -0,0 +1,10 @@
/**
* @file
* Caption filter: default styling for displaying Media Embed captions.
*/
.caption .media .field,
.caption .media .field * {
float: none;
margin: unset;
}

View File

@ -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

View File

@ -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>&lt;drupal-media&gt;</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>&lt;drupal-media&gt;</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);
}
}

View File

@ -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>&lt;drupal-media data-entity-uuid="07bf3a2e-1941-4a44-9b02-2d1d7a41ec0e" /&gt;</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>&lt;drupal-media&gt;</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'];
}
}

View File

@ -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

View File

@ -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();
}

View File

@ -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);
}
}

View File

@ -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]',
],
];
}
}

View File

@ -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;
}
}

View File

@ -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;
}
}

View File

@ -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,
];
}
}
}

View File

@ -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;

View File

@ -0,0 +1,10 @@
/**
* @file
* Caption filter: default styling for displaying Media Embed captions.
*/
.caption .media .field,
.caption .media .field * {
float: none;
margin: unset;
}

View File

@ -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: