...onditionalCacheabilityMetadataBubblingTrait.php | 32 ++++ core/modules/filter/src/Element/ProcessedText.php | 7 +- core/modules/filter/src/FilterProcessResult.php | 2 +- .../tests/src/Kernel/EntitySerializationTest.php | 34 +++- .../text/src/Normalizer/TextItemBaseNormalizer.php | 101 ++++++++++++ .../Normalizer/TextItemBaseNormalizerTest.php | 183 +++++++++++++++++++++ core/modules/text/text.services.yml | 8 + 7 files changed, 361 insertions(+), 6 deletions(-) diff --git a/core/lib/Drupal/Core/Cache/ConditionalCacheabilityMetadataBubblingTrait.php b/core/lib/Drupal/Core/Cache/ConditionalCacheabilityMetadataBubblingTrait.php new file mode 100644 index 0000000..b8c4b33 --- /dev/null +++ b/core/lib/Drupal/Core/Cache/ConditionalCacheabilityMetadataBubblingTrait.php @@ -0,0 +1,32 @@ +renderer->hasRenderContext()) { + $build = []; + CacheableMetadata::createFromObject($object)->applyTo($build); + $this->renderer->render($build); + } + } + +} diff --git a/core/modules/filter/src/Element/ProcessedText.php b/core/modules/filter/src/Element/ProcessedText.php index 0d09870..27b797e 100644 --- a/core/modules/filter/src/Element/ProcessedText.php +++ b/core/modules/filter/src/Element/ProcessedText.php @@ -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,11 @@ 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'); + CacheableMetadata::createFromRenderArray($element) + ->addCacheableDependency($filter_settings) + ->applyTo($element); } /** @var \Drupal\filter\Entity\FilterFormat $format **/ $format = FilterFormat::load($format_id); diff --git a/core/modules/filter/src/FilterProcessResult.php b/core/modules/filter/src/FilterProcessResult.php index 4b07fad..02c89d0 100644 --- a/core/modules/filter/src/FilterProcessResult.php +++ b/core/modules/filter/src/FilterProcessResult.php @@ -77,7 +77,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; } diff --git a/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php b/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php index 0f84d3d..ad23a04 100644 --- a/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php +++ b/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php @@ -4,6 +4,7 @@ use Drupal\Component\Utility\SafeMarkup; use Drupal\entity_test\Entity\EntityTestMulRev; +use Drupal\filter\Entity\FilterFormat; /** * Tests that entities can be serialized to supported core formats. @@ -60,6 +61,29 @@ protected function setUp() { // User create needs sequence table. $this->installSchema('system', array('sequences')); + // Create a text format because it is needed for TextItemBase normalization. + // @see \Drupal\text\Normalizer\TextItemBaseNormalizer::normalize(). + FilterFormat::create([ + 'format' => 'my_text_format', + 'name' => 'My Text Format', + 'filters' => [ + 'filter_html' => [ + 'module' => 'filter', + 'status' => TRUE, + 'weight' => 10, + 'settings' => [ + 'allowed_html' => '

', + ], + ], + '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', @@ -69,12 +93,13 @@ protected function setUp() { $this->user->save(); // Create a test entity to serialize. + $test_text_value = $this->randomMachineName(); $this->values = array( 'name' => $this->randomMachineName(), 'user_id' => $this->user->id(), 'field_test_text' => array( - 'value' => $this->randomMachineName(), - 'format' => 'full_html', + 'value' => $test_text_value, + 'format' => 'my_text_format', ), ); $this->entity = EntityTestMulRev::create($this->values); @@ -127,6 +152,7 @@ public function testNormalize() { array( 'value' => $this->values['field_test_text']['value'], 'format' => $this->values['field_test_text']['format'], + 'processed' => "

{$this->values['field_test_text']['value']}

" ), ), ); @@ -134,7 +160,7 @@ public function testNormalize() { $normalized = $this->serializer->normalize($this->entity); foreach (array_keys($expected) as $fieldName) { - $this->assertEqual($expected[$fieldName], $normalized[$fieldName], "ComplexDataNormalizer produces expected array for $fieldName."); + $this->assertEquals($expected[$fieldName], $normalized[$fieldName], "Field normalization produces expected array for $fieldName."); } $this->assertEqual(array_diff_key($normalized, $expected), array(), 'No unexpected data is added to the normalized array.'); } @@ -192,7 +218,7 @@ public function testSerialize() { 'revision_id' => '' . $this->entity->getRevisionId() . '', 'default_langcode' => '1', 'non_rev_field' => '', - 'field_test_text' => '' . $this->values['field_test_text']['value'] . '' . $this->values['field_test_text']['format'] . '', + 'field_test_text' => '' . $this->values['field_test_text']['value'] . '' . $this->values['field_test_text']['format'] . '' . $this->values['field_test_text']['value'] . '

]]>
', ); // Sort it in the same order as normalised. $expected = array_merge($normalized, $expected); diff --git a/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php b/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php new file mode 100644 index 0000000..a1701a6 --- /dev/null +++ b/core/modules/text/src/Normalizer/TextItemBaseNormalizer.php @@ -0,0 +1,101 @@ +renderer = $renderer; + $this->configFactory = $config_factory; + } + + /** + * {@inheritdoc} + */ + public function normalize($field_item, $format = NULL, array $context = []) { + $attributes = parent::normalize($field_item, $format, $context); + $processed_text = $this->computeProcessedAttribute($field_item); + $this->bubble($processed_text); + $attributes['processed'] = $this->serializer->normalize($processed_text->getProcessedText(), $format, $context); + return $attributes; + } + + /** + * Computes the 'processed' property of TextItemBase fields. + * + * The 'processed' property for fields that extend TextItemBase will + * not contain cacheability metadata + * (see \Drupal\text\TextProcessed::getValue()). Therefore this function + * returns a FilterProcessResult which can carry both the processed text and + * the cacheability metadata. The cacheability metadata can be bubbled up in + * GET responses that use this normalization so that the responses can be + * cached (and invalidated) correctly. + * + * @param \Drupal\text\Plugin\Field\FieldType\TextItemBase $field_item + * The field item. + * + * @return \Drupal\filter\FilterProcessResult + * The filter process result for the text item. + * + * @see \Drupal\text\TextProcessed::getValue() + * @see \Drupal\text\Plugin\Field\FieldType\TextItemBase::propertyDefinitions() + */ + protected function computeProcessedAttribute(TextItemBase $field_item) { + $value = $field_item->getValue(); + $build = [ + '#type' => 'processed_text', + '#text' => $value['value'], + '#format' => $value['format'], + '#filter_types_to_skip' => [], + '#langcode' => '', + ]; + // It's necessary to capture the cacheability metadata associated with the + // processed text. See https://www.drupal.org/node/2278483. + $processed_text = $this->renderer->renderPlain($build); + return FilterProcessResult::createFromRenderArray($build)->setProcessedText($processed_text); + } + +} diff --git a/core/modules/text/tests/src/Kernel/Normalizer/TextItemBaseNormalizerTest.php b/core/modules/text/tests/src/Kernel/Normalizer/TextItemBaseNormalizerTest.php new file mode 100644 index 0000000..0d217a2 --- /dev/null +++ b/core/modules/text/tests/src/Kernel/Normalizer/TextItemBaseNormalizerTest.php @@ -0,0 +1,183 @@ +serializer = \Drupal::service('serializer'); + + $this->installEntitySchema('entity_test'); + $this->installEntitySchema('user'); + $this->installConfig('filter'); + + FieldStorageConfig::create([ + 'entity_type' => 'entity_test', + 'field_name' => 'field_text', + 'type' => 'text', + ])->save(); + + FieldConfig::create([ + 'entity_type' => 'entity_test', + 'field_name' => 'field_text', + 'bundle' => 'entity_test', + ])->save(); + + FilterFormat::create([ + 'format' => 'my_text_format', + 'name' => 'My text format', + 'filters' => [ + 'filter_autop' => [ + 'module' => 'filter', + 'status' => TRUE, + ], + 'filter_html' => [ + 'status' => TRUE, + 'settings' => [ + 'allowed_html' => '', + ], + ], + // Include this test filter because it bubbles cache tags. + 'filter_test_cache_tags' => [ + 'status' => TRUE, + ], + // Include this test filter because it bubbles cache contexts. + 'filter_test_cache_contexts' => [ + 'status' => TRUE, + ], + ], + ])->save(); + } + + /** + * @covers ::normalize + * + * @dataProvider testNormalizeProvider + */ + public function testNormalize($text_item, array $expected, CacheableMetadata $extra_cacheability, array $filter_config_update = [], $updated_processed = '') { + $original_entity = EntityTest::create(['field_text' => $text_item]); + $original_entity->save(); + $text_format = FilterFormat::load(empty($text_item['format']) ? static::$fallbackFormatId : $text_item['format']); + + $entity = clone $original_entity; + $context = new RenderContext(); + $data = $this->container->get('renderer') + ->executeInRenderContext($context, function () use ($entity) { + return $this->serializer->normalize($entity); + }); + + $expected_cacheability = new BubbleableMetadata(); + $expected_cacheability->setCacheTags($text_format->getCacheTags()); + $contexts = $this->container->getParameter('renderer.config')['required_cache_contexts']; + $expected_cacheability->setCacheContexts($contexts); + // Merge the CacheableMetadata that is specific to this test. + $expected_cacheability = $expected_cacheability->merge($extra_cacheability); + + $this->assertEquals($expected_cacheability, $context->pop()); + $this->assertEquals($expected, $data['field_text'][0]); + + if ($filter_config_update) { + // Update format to see if normalization changes. + foreach ($filter_config_update as $instance_id => $update) { + $text_format->setFilterConfig($instance_id, $update); + } + $text_format->save(); + + $entity = clone $original_entity; + $context = new RenderContext(); + $data = $this->container->get('renderer') + ->executeInRenderContext($context, function () use ($entity) { + return $this->serializer->normalize($entity); + }); + + $this->assertFalse($context->isEmpty()); + $this->assertEquals($expected_cacheability, $context->pop()); + $expected['processed'] = $updated_processed; + $this->assertEquals($expected, $data['field_text'][0]); + } + } + + /** + * Data provider for testNormalize(). + */ + public function testNormalizeProvider() { + $test_cases['no format specified'] = [ + 'text item', + [ + 'value' => 'text item', + 'processed' => "

text item

\n", + 'format' => NULL, + ], + (new CacheableMetadata()) + ->setCacheTags(['config:filter.settings']), + ]; + $test_cases['my text format'] = [ + [ + 'value' => 'This is important.', + 'format' => 'my_text_format', + ], + [ + 'value' => "This is important.", + 'format' => 'my_text_format', + 'processed' => "

This is important.

\n", + ], + (new CacheableMetadata()) + // Add the tags for the 'filter_test_cache_tags' filter. + ->setCacheTags(['foo:bar', 'foo:baz']) + // Add the contexts for the 'filter_test_cache_contexts' filter. + ->setCacheContexts(['languages:' . LanguageInterface::TYPE_CONTENT]), + [ + 'filter_html' => [ + 'status' => TRUE, + 'settings' => [ + 'allowed_html' => ' ', + ], + ], + ], + "

This is important.

\n", + ]; + return $test_cases; + } + +} diff --git a/core/modules/text/text.services.yml b/core/modules/text/text.services.yml new file mode 100644 index 0000000..0d13007 --- /dev/null +++ b/core/modules/text/text.services.yml @@ -0,0 +1,8 @@ +services: + serializer.normalizer.text_item_base: + class: Drupal\text\Normalizer\TextItemBaseNormalizer + arguments: ['@renderer', '@config.factory'] + tags: + # This normalizer needs to be the same or greater priority than serializer.normalizer.field_item.hal + - { name: normalizer, priority: 10 } +