Commit 9ba78242 authored by larowlan's avatar larowlan

Issue #2626924 by Wim Leers, tedbow, dawehner, frob, martin107, damiankloip,...

Issue #2626924 by Wim Leers, tedbow, dawehner, frob, martin107, damiankloip, dagmar, almaudoh, Berdir, larowlan, amateescu: Include processed text in normalizations: "text" field type's "processed" computed property should be non-internal and carry cacheability metadata
parent efab31b7
......@@ -3,6 +3,7 @@
namespace Drupal\filter\Element;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Cache\CacheableMetadata;
use Drupal\Core\Render\BubbleableMetadata;
use Drupal\Core\Render\Element\RenderElement;
use Drupal\filter\Entity\FilterFormat;
......@@ -69,7 +70,12 @@ public static function preRenderText($element) {
$langcode = $element['#langcode'];
if (!isset($format_id)) {
$format_id = static::configFactory()->get('filter.settings')->get('fallback_format');
$filter_settings = static::configFactory()->get('filter.settings');
$format_id = $filter_settings->get('fallback_format');
// Ensure 'filter.settings' config's cacheability is respected.
CacheableMetadata::createFromRenderArray($element)
->addCacheableDependency($filter_settings)
->applyTo($element);
}
/** @var \Drupal\filter\Entity\FilterFormat $format **/
$format = FilterFormat::load($format_id);
......
......@@ -78,7 +78,7 @@ class FilterProcessResult extends BubbleableMetadata {
* @param string $processed_text
* The text as processed by a text filter.
*/
public function __construct($processed_text) {
public function __construct($processed_text = '') {
$this->processedText = $processed_text;
}
......
......@@ -5,6 +5,7 @@
use Drupal\Core\Entity\EntityInterface;
use Drupal\Core\Url;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\filter\Entity\FilterFormat;
/**
* Tests HAL normalization edge cases for EntityResource.
......@@ -19,6 +20,27 @@ class NormalizeTest extends NormalizerTestBase {
protected function setUp() {
parent::setUp();
FilterFormat::create([
'format' => 'my_text_format',
'name' => 'My Text Format',
'filters' => [
'filter_html' => [
'module' => 'filter',
'status' => TRUE,
'weight' => 10,
'settings' => [
'allowed_html' => '<p>',
],
],
'filter_autop' => [
'module' => 'filter',
'status' => TRUE,
'weight' => 10,
'settings' => [],
],
],
])->save();
\Drupal::service('router.builder')->rebuild();
}
......@@ -37,7 +59,7 @@ public function testNormalize() {
'name' => $this->randomMachineName(),
'field_test_text' => [
'value' => $this->randomMachineName(),
'format' => 'full_html',
'format' => 'my_text_format',
],
'field_test_entity_reference' => [
'target_id' => $target_entity_de->id(),
......@@ -152,6 +174,7 @@ public function testNormalize() {
[
'value' => $values['field_test_text']['value'],
'format' => $values['field_test_text']['format'],
'processed' => "<p>{$values['field_test_text']['value']}</p>",
],
],
];
......
......@@ -4,6 +4,7 @@
use Drupal\block_content\Entity\BlockContent;
use Drupal\block_content\Entity\BlockContentType;
use Drupal\Core\Cache\Cache;
use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
......@@ -131,6 +132,7 @@ protected function getExpectedNormalizedEntity() {
'value' => 'The name "llama" was adopted by European settlers from native Peruvians.',
'format' => 'plain_text',
'summary' => NULL,
'processed' => "<p>The name &quot;llama&quot; was adopted by European settlers from native Peruvians.</p>\n",
],
],
'status' => [
......@@ -180,4 +182,18 @@ protected function getExpectedUnauthorizedAccessCacheability() {
->addCacheTags(['block_content:1']);
}
/**
* {@inheritdoc}
*/
protected function getExpectedCacheTags() {
return Cache::mergeTags(parent::getExpectedCacheTags(), ['config:filter.format.plain_text']);
}
/**
* {@inheritdoc}
*/
protected function getExpectedCacheContexts() {
return Cache::mergeContexts(['url.site'], $this->container->getParameter('renderer.config')['required_cache_contexts']);
}
}
......@@ -5,6 +5,7 @@
use Drupal\comment\Entity\Comment;
use Drupal\comment\Entity\CommentType;
use Drupal\comment\Tests\CommentTestTrait;
use Drupal\Core\Cache\Cache;
use Drupal\entity_test\Entity\EntityTest;
use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
use Drupal\Tests\rest\Functional\EntityResource\EntityResourceTestBase;
......@@ -197,6 +198,7 @@ protected function getExpectedNormalizedEntity() {
[
'value' => 'The name "llama" was adopted by European settlers from native Peruvians.',
'format' => 'plain_text',
'processed' => '<p>The name &quot;llama&quot; was adopted by European settlers from native Peruvians.</p>' . "\n",
],
],
];
......@@ -248,6 +250,20 @@ protected function getNormalizedPatchEntity() {
return array_diff_key($this->getNormalizedPostEntity(), ['entity_type' => TRUE, 'entity_id' => TRUE, 'field_name' => TRUE]);
}
/**
* {@inheritdoc}
*/
protected function getExpectedCacheTags() {
return Cache::mergeTags(parent::getExpectedCacheTags(), ['config:filter.format.plain_text']);
}
/**
* {@inheritdoc}
*/
protected function getExpectedCacheContexts() {
return Cache::mergeContexts(['languages:language_interface', 'theme'], parent::getExpectedCacheContexts());
}
/**
* Tests POSTing a comment without critical base fields.
*
......
<?php
namespace Drupal\Tests\rest\Functional\EntityResource\EntityTest;
use Drupal\Core\Cache\Cache;
use Drupal\Core\Language\LanguageInterface;
use Drupal\filter\Entity\FilterFormat;
use Drupal\Tests\rest\Functional\AnonResourceTestTrait;
/**
* @group rest
*/
class EntityTestTextItemNormalizerTest extends EntityTestResourceTestBase {
use AnonResourceTestTrait;
/**
* {@inheritdoc}
*/
public static $modules = ['filter_test'];
/**
* {@inheritdoc}
*/
protected static $format = 'json';
/**
* {@inheritdoc}
*/
protected static $mimeType = 'application/json';
/**
* {@inheritdoc}
*/
protected function setUpAuthorization($method) {
parent::setUpAuthorization($method);
if (in_array($method, ['POST', 'PATCH'], TRUE)) {
$this->grantPermissionsToTestedRole(['use text format my_text_format']);
}
}
/**
* {@inheritdoc}
*/
protected function getExpectedNormalizedEntity() {
$expected = parent::getExpectedNormalizedEntity();
$expected['field_test_text'] = [
[
'value' => 'Cádiz is the oldest continuously inhabited city in Spain and a nice place to spend a Sunday with friends.',
'format' => 'my_text_format',
'processed' => '<p>Cádiz is the oldest continuously inhabited city in Spain and a nice place to spend a Sunday with friends.</p>' . "\n" . '<p>This is a dynamic llama.</p>',
],
];
return $expected;
}
/**
* {@inheritdoc}
*/
protected function createEntity() {
$entity = parent::createEntity();
if (!FilterFormat::load('my_text_format')) {
FilterFormat::create([
'format' => 'my_text_format',
'name' => 'My Text Format',
'filters' => [
'filter_test_assets' => [
'weight' => -1,
'status' => TRUE,
],
'filter_test_cache_tags' => [
'weight' => 0,
'status' => TRUE,
],
'filter_test_cache_contexts' => [
'weight' => 0,
'status' => TRUE,
],
'filter_test_cache_merge' => [
'weight' => 0,
'status' => TRUE,
],
'filter_test_placeholders' => [
'weight' => 1,
'status' => TRUE,
],
'filter_autop' => [
'status' => TRUE,
],
],
])->save();
}
$entity->field_test_text = [
'value' => 'Cádiz is the oldest continuously inhabited city in Spain and a nice place to spend a Sunday with friends.',
'format' => 'my_text_format',
];
$entity->save();
return $entity;
}
/**
* {@inheritdoc}
*/
protected function getNormalizedPostEntity() {
$post_entity = parent::getNormalizedPostEntity();
$post_entity['field_test_text'] = [
[
'value' => 'Llamas are awesome.',
'format' => 'my_text_format',
],
];
return $post_entity;
}
/**
* {@inheritdoc}
*/
protected function getExpectedCacheTags() {
return Cache::mergeTags([
// The cache tag set by the processed_text element itself.
'config:filter.format.my_text_format',
// The cache tags set by the filter_test_cache_tags filter.
'foo:bar',
'foo:baz',
// The cache tags set by the filter_test_cache_merge filter.
'merge:tag'
], parent::getExpectedCacheTags());
}
/**
* {@inheritdoc}
*/
protected function getExpectedCacheContexts() {
return Cache::mergeContexts([
// The cache context set by the filter_test_cache_contexts filter.
'languages:' . LanguageInterface::TYPE_CONTENT,
// The default cache contexts for Renderer.
'languages:' . LanguageInterface::TYPE_INTERFACE,
'theme',
// The cache tags set by the filter_test_cache_merge filter.
'user.permissions',
], parent::getExpectedCacheContexts());
}
/**
* Tests GETting an entity with the test text field set to a specific format.
*
* @dataProvider providerTestGetWithFormat
*/
public function testGetWithFormat($text_format_id, array $expected_cache_tags) {
FilterFormat::create([
'name' => 'Pablo Piccasso',
'format' => 'pablo',
'langcode' => 'es',
'filters' => [],
])->save();
// Set TextItemBase field's value for testing, using the given text format.
$value = [
'value' => $this->randomString(),
];
if ($text_format_id !== FALSE) {
$value['format'] = $text_format_id;
}
$this->entity->set('field_test_text', $value)->save();
$this->initAuthentication();
$url = $this->getEntityResourceUrl();
$url->setOption('query', ['_format' => static::$format]);
$request_options = $this->getAuthenticationRequestOptions('GET');
$this->provisionEntityResource();
$this->setUpAuthorization('GET');
$response = $this->request('GET', $url, $request_options);
$expected_cache_tags = Cache::mergeTags($expected_cache_tags, parent::getExpectedCacheTags());
$this->assertSame($expected_cache_tags, explode(' ', $response->getHeader('X-Drupal-Cache-Tags')[0]));
}
public function providerTestGetWithFormat() {
return [
'format specified (different from fallback format)' => [
'pablo',
['config:filter.format.pablo'],
],
'format specified (happens to be the same as fallback format)' => [
'plain_text',
['config:filter.format.plain_text'],
],
'no format specified: fallback format used automatically' => [
FALSE,
['config:filter.format.plain_text', 'config:filter.settings'],
],
];
}
}
......@@ -2,6 +2,7 @@
namespace Drupal\Tests\rest\Functional\EntityResource\Term;
use Drupal\Core\Cache\Cache;
use Drupal\taxonomy\Entity\Term;
use Drupal\taxonomy\Entity\Vocabulary;
use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
......@@ -79,6 +80,7 @@ protected function createEntity() {
// Create a "Llama" taxonomy term.
$term = Term::create(['vid' => $vocabulary->id()])
->setName('Llama')
->setDescription("It is a little known fact that llamas cannot count higher than seven.")
->setChangedTime(123456789)
->set('path', '/llama');
$term->save();
......@@ -109,8 +111,9 @@ protected function getExpectedNormalizedEntity() {
],
'description' => [
[
'value' => NULL,
'value' => 'It is a little known fact that llamas cannot count higher than seven.',
'format' => NULL,
'processed' => "<p>It is a little known fact that llamas cannot count higher than seven.</p>\n",
],
],
'parent' => [],
......@@ -230,4 +233,18 @@ public function testPatchPath() {
$this->assertSame($normalization['path'], $updated_normalization['path']);
}
/**
* {@inheritdoc}
*/
protected function getExpectedCacheTags() {
return Cache::mergeTags(parent::getExpectedCacheTags(), ['config:filter.format.plain_text', 'config:filter.settings']);
}
/**
* {@inheritdoc}
*/
protected function getExpectedCacheContexts() {
return Cache::mergeContexts(['url.site'], $this->container->getParameter('renderer.config')['required_cache_contexts']);
}
}
......@@ -19,7 +19,12 @@ class TypedDataNormalizer extends NormalizerBase {
*/
public function normalize($object, $format = NULL, array $context = []) {
$this->addCacheableDependency($context, $object);
return $object->getValue();
$value = $object->getValue();
// Support for stringable value objects: avoid numerous custom normalizers.
if (is_object($value) && method_exists($value, '__toString')) {
$value = (string) $value;
}
return $value;
}
}
......@@ -2,8 +2,10 @@
namespace Drupal\Tests\serialization\Kernel;
use Drupal\Component\Serialization\Json;
use Drupal\Component\Utility\SafeMarkup;
use Drupal\entity_test\Entity\EntityTestMulRev;
use Drupal\filter\Entity\FilterFormat;
use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait;
/**
......@@ -63,6 +65,27 @@ protected function setUp() {
// User create needs sequence table.
$this->installSchema('system', ['sequences']);
FilterFormat::create([
'format' => 'my_text_format',
'name' => 'My Text Format',
'filters' => [
'filter_html' => [
'module' => 'filter',
'status' => TRUE,
'weight' => 10,
'settings' => [
'allowed_html' => '<p>',
],
],
'filter_autop' => [
'module' => 'filter',
'status' => TRUE,
'weight' => 10,
'settings' => [],
],
],
])->save();
// Create a test user to use as the entity owner.
$this->user = \Drupal::entityManager()->getStorage('user')->create([
'name' => 'serialization_test_user',
......@@ -72,12 +95,13 @@ protected function setUp() {
$this->user->save();
// Create a test entity to serialize.
$test_text_value = $this->randomMachineName();
$this->values = [
'name' => $this->randomMachineName(),
'user_id' => $this->user->id(),
'field_test_text' => [
'value' => $this->randomMachineName(),
'format' => 'full_html',
'value' => $test_text_value,
'format' => 'my_text_format',
],
];
$this->entity = EntityTestMulRev::create($this->values);
......@@ -134,6 +158,7 @@ public function testNormalize() {
[
'value' => $this->values['field_test_text']['value'],
'format' => $this->values['field_test_text']['format'],
'processed' => "<p>{$this->values['field_test_text']['value']}</p>",
],
],
];
......@@ -174,7 +199,7 @@ public function testSerialize() {
// JsonEncoder. The output of ComplexDataNormalizer::normalize() is tested
// elsewhere, so we can just assume that it works properly here.
$normalized = $this->serializer->normalize($this->entity, 'json');
$expected = json_encode($normalized);
$expected = Json::encode($normalized);
// Test 'json'.
$actual = $this->serializer->serialize($this->entity, 'json');
$this->assertIdentical($actual, $expected, 'Entity serializes to JSON when "json" is requested.');
......@@ -202,7 +227,7 @@ public function testSerialize() {
'default_langcode' => '<default_langcode><value>1</value></default_langcode>',
'revision_translation_affected' => '<revision_translation_affected><value>1</value></revision_translation_affected>',
'non_rev_field' => '<non_rev_field/>',
'field_test_text' => '<field_test_text><value>' . $this->values['field_test_text']['value'] . '</value><format>' . $this->values['field_test_text']['format'] . '</format></field_test_text>',
'field_test_text' => '<field_test_text><value>' . $this->values['field_test_text']['value'] . '</value><format>' . $this->values['field_test_text']['format'] . '</format><processed><![CDATA[<p>' . $this->values['field_test_text']['value'] . '</p>]]></processed></field_test_text>',
];
// Sort it in the same order as normalised.
$expected = array_merge($normalized, $expected);
......
......@@ -29,7 +29,8 @@ public static function propertyDefinitions(FieldStorageDefinitionInterface $fiel
->setDescription(t('The text with the text format applied.'))
->setComputed(TRUE)
->setClass('\Drupal\text\TextProcessed')
->setSetting('text source', 'value');
->setSetting('text source', 'value')
->setInternal(FALSE);
return $properties;
}
......
......@@ -2,9 +2,12 @@
namespace Drupal\text;
use Drupal\Core\Cache\CacheableDependencyInterface;
use Drupal\Core\TypedData\DataDefinitionInterface;
use Drupal\Core\TypedData\TypedDataInterface;
use Drupal\Core\TypedData\TypedData;
use Drupal\filter\FilterProcessResult;
use Drupal\filter\Render\FilteredMarkup;
/**
* A computed property for processing text with a format.
......@@ -12,12 +15,12 @@
* Required settings (below the definition's 'settings' key) are:
* - text source: The text property containing the to be processed text.
*/
class TextProcessed extends TypedData {
class TextProcessed extends TypedData implements CacheableDependencyInterface {
/**
* Cached processed text.
*
* @var string|null
* @var \Drupal\filter\FilterProcessResult|null
*/
protected $processed = NULL;
......@@ -37,20 +40,29 @@ public function __construct(DataDefinitionInterface $definition, $name = NULL, T
*/
public function getValue() {
if ($this->processed !== NULL) {
return $this->processed;
return FilteredMarkup::create($this->processed->getProcessedText());
}
$item = $this->getParent();
$text = $item->{($this->definition->getSetting('text source'))};
// Avoid running check_markup() on empty strings.
// Avoid doing unnecessary work on empty strings.
if (!isset($text) || $text === '') {
$this->processed = '';
$this->processed = new FilterProcessResult('');
}
else {
$this->processed = check_markup($text, $item->format, $item->getLangcode());
$build = [
'#type' => 'processed_text',
'#text' => $text,
'#format' => $item->format,
'#filter_types_to_skip' => [],
'#langcode' => $item->getLangcode(),
];
// Capture the cacheability metadata associated with the processed text.
$processed_text = $this->getRenderer()->renderPlain($build);
$this->processed = FilterProcessResult::createFromRenderArray($build)->setProcessedText((string) $processed_text);
}
return $this->processed;
return FilteredMarkup::create($this->processed->getProcessedText());
}
/**
......@@ -64,4 +76,37 @@ public function setValue($value, $notify = TRUE) {
}
}
/**
* {@inheritdoc}
*/
public function getCacheTags() {
$this->getValue();
return $this->processed->getCacheTags();
}
/**
* {@inheritdoc}
*/
public function getCacheContexts() {
$this->getValue();
return $this->processed->getCacheContexts();
}
/**
* {@inheritdoc}
*/
public function getCacheMaxAge() {
$this->getValue();
return $this->processed->getCacheMaxAge();
}
/**
* Returns the renderer service.
*
* @return \Drupal\Core\Render\RendererInterface
*/
protected function getRenderer() {
return \Drupal::service('renderer');
}
}
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