Commit 86736407 authored by effulgentsia's avatar effulgentsia

Issue #2940029 by Wim Leers, krlucas, legovaer, vijaycs85, nathandentzau,...

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 3bb50b2b
/**
* @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>&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);
}
}
This diff is collapsed.
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
<?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();
}
<?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]',
],
];
}
}
This diff is collapsed.
<?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;
}
}
<?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;
......
/**
* @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:
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment