diff --git a/core/lib/Drupal/Core/TypedData/DataDefinition.php b/core/lib/Drupal/Core/TypedData/DataDefinition.php index 52a4394cd7..6a5fb03861 100644 --- a/core/lib/Drupal/Core/TypedData/DataDefinition.php +++ b/core/lib/Drupal/Core/TypedData/DataDefinition.php @@ -5,7 +5,7 @@ /** * A typed data definition class for defining data based on defined data types. */ -class DataDefinition implements DataDefinitionInterface, \ArrayAccess { +class DataDefinition implements DataDefinitionInterface, ExposableDataDefinitionInterface, \ArrayAccess { use TypedDataTrait; @@ -150,6 +150,21 @@ public function isComputed() { } /** + * {@inheritdoc} + */ + public function setExposed($computed) { + $this->definition['exposed'] = $computed; + return $this; + } + + /** + * {@inheritdoc} + */ + public function isExposed() { + return !empty($this->definition['exposed']); + } + + /** * Sets whether the data is computed. * * @param bool $computed diff --git a/core/lib/Drupal/Core/TypedData/ExposableDataDefinitionInterface.php b/core/lib/Drupal/Core/TypedData/ExposableDataDefinitionInterface.php new file mode 100644 index 0000000000..5d3093d883 --- /dev/null +++ b/core/lib/Drupal/Core/TypedData/ExposableDataDefinitionInterface.php @@ -0,0 +1,31 @@ +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 3fa592b627..2a8a71bd1f 100644 --- a/core/modules/filter/src/FilterProcessResult.php +++ b/core/modules/filter/src/FilterProcessResult.php @@ -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; } diff --git a/core/modules/hal/src/Normalizer/FieldItemNormalizer.php b/core/modules/hal/src/Normalizer/FieldItemNormalizer.php index dc5aec9899..cf4769a58d 100644 --- a/core/modules/hal/src/Normalizer/FieldItemNormalizer.php +++ b/core/modules/hal/src/Normalizer/FieldItemNormalizer.php @@ -3,6 +3,7 @@ namespace Drupal\hal\Normalizer; use Drupal\Core\Field\FieldItemInterface; +use Drupal\serialization\Normalizer\ComputedPropertiesNormalizerTrait; use Symfony\Component\Serializer\Exception\InvalidArgumentException; /** @@ -10,6 +11,8 @@ */ class FieldItemNormalizer extends NormalizerBase { + use ComputedPropertiesNormalizerTrait; + /** * The interface or class that this Normalizer supports. * @@ -21,6 +24,7 @@ class FieldItemNormalizer extends NormalizerBase { * {@inheritdoc} */ public function normalize($field_item, $format = NULL, array $context = []) { + /** @var \Drupal\Core\Field\FieldItemInterface $field_item */ $values = []; // We normalize each individual property, so each can do their own casting, // if needed. @@ -29,6 +33,8 @@ public function normalize($field_item, $format = NULL, array $context = []) { $values[$property_name] = $this->serializer->normalize($property, $format, $context); } + $values = array_merge($values, $this->getComputedAttributes($field_item, $format, $context)); + if (isset($context['langcode'])) { $values['lang'] = $context['langcode']; } diff --git a/core/modules/hal/tests/src/Kernel/NormalizeTest.php b/core/modules/hal/tests/src/Kernel/NormalizeTest.php index d837b79a02..e77d4b464a 100644 --- a/core/modules/hal/tests/src/Kernel/NormalizeTest.php +++ b/core/modules/hal/tests/src/Kernel/NormalizeTest.php @@ -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. @@ -18,6 +19,28 @@ class NormalizeTest extends NormalizerTestBase { */ protected function setUp() { parent::setUp(); + // 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(); \Drupal::service('router.builder')->rebuild(); } @@ -37,7 +60,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 +175,7 @@ public function testNormalize() { [ 'value' => $values['field_test_text']['value'], 'format' => $values['field_test_text']['format'], + 'process_result' => "

{$values['field_test_text']['value']}

", ], ], ]; diff --git a/core/modules/rest/src/EventSubscriber/ResourceResponseSubscriber.php b/core/modules/rest/src/EventSubscriber/ResourceResponseSubscriber.php index 86935ea1cc..c1b92834af 100644 --- a/core/modules/rest/src/EventSubscriber/ResourceResponseSubscriber.php +++ b/core/modules/rest/src/EventSubscriber/ResourceResponseSubscriber.php @@ -2,6 +2,7 @@ namespace Drupal\rest\EventSubscriber; +use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\Cache\CacheableResponse; use Drupal\Core\Cache\CacheableResponseInterface; use Drupal\Core\Render\RenderContext; @@ -126,11 +127,18 @@ public function getResponseFormat(RouteMatchInterface $route_match, Request $req /** * Renders a resource response body. * - * Serialization can invoke rendering (e.g., generating URLs), but the - * serialization API does not provide a mechanism to collect the - * bubbleable metadata associated with that (e.g., language and other - * contexts), so instead, allow those to "leak" and collect them here in - * a render context. + * During serialization, encoders and normalizers are able to explicitly + * bubble cacheability metadata via the 'cacheability' key-value pair in the + * received context. This bubbled cacheability metadata will be applied to the + * the response. + * + * In prior versions of Drupal 8, we allowed implicit bubbling of cacheability + * metadata because there was no explicit cacheability metadata bubbling API. + * To maintain backwards compatibility, we continue to support this, but + * support for this will be dropped in Drupal 9.0.0. This is especially useful + * when interacting with APIs that implicitly invoke rendering (for example: + * generating URLs): this allows those to "leak", and we collect their bubbled + * cacheability metadata automatically in a render context. * * @param \Symfony\Component\HttpFoundation\Request $request * The request object. @@ -150,14 +158,25 @@ protected function renderResponseBody(Request $request, ResourceResponseInterfac // If there is data to send, serialize and set it as the response body. if ($data !== NULL) { + $serialization_context = [ + 'request' => $request, + 'cacheability' => new CacheableMetadata(), + ]; + + // @deprecated In Drupal 8.4.0, will be removed before Drupal 9.0.0. Use + // explicit cacheability metadata bubbling instead. (The wrapping call to + // executeInRenderContext() will be removed before Drupal 9.0.0.) $context = new RenderContext(); $output = $this->renderer - ->executeInRenderContext($context, function () use ($serializer, $data, $format) { - return $serializer->serialize($data, $format); + ->executeInRenderContext($context, function() use ($serializer, $data, $format, $serialization_context) { + return $serializer->serialize($data, $format, $serialization_context); }); - - if ($response instanceof CacheableResponseInterface && !$context->isEmpty()) { - $response->addCacheableDependency($context->pop()); + if ($response instanceof CacheableResponseInterface) { + if (!$context->isEmpty()) { + @trigger_error('Implicit cacheability metadata bubbling (onto the global render context) in normalizers is deprecated since Drupal 8.4.0 and will be removed in Drupal 9.0.0. Use the "cacheability" serialization context instead, for explicit cacheability metadata bubbling.', E_USER_DEPRECATED); + $response->addCacheableDependency($context->pop()); + } + $response->addCacheableDependency($serialization_context['cacheability']); } $response->setContent($output); diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php index a7317279c7..0244f003c6 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php @@ -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\EntityResource\EntityResourceTestBase; use Drupal\user\Entity\User; @@ -200,6 +201,7 @@ protected function getExpectedNormalizedEntity() { [ 'value' => 'The name "llama" was adopted by European settlers from native Peruvians.', 'format' => 'plain_text', + 'process_result' => '

The name "llama" was adopted by European settlers from native Peruvians.

' . "\n", ], ], ]; @@ -252,6 +254,20 @@ protected function getNormalizedPatchEntity() { } /** + * {@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. * * testPost() is testing with the most minimal normalization possible: the one diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php index a55ae929e9..3726b4cdd9 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php @@ -411,9 +411,9 @@ public function testGet() { // for the keys with the array order the same (it needs to match with // identical comparison). $expected = $this->getExpectedNormalizedEntity(); - ksort($expected); + $this->nestedKsort($expected); $actual = $this->serializer->decode((string) $response->getBody(), static::$format); - ksort($actual); + $this->nestedKsort($actual); $this->assertSame($expected, $actual); // Not only assert the normalization, also assert deserialization of the @@ -474,9 +474,9 @@ public function testGet() { // Config entities are not affected. // @see \Drupal\serialization\Normalizer\ConfigEntityNormalizer::normalize() $expected = static::castToString($expected); - ksort($expected); + $this->nestedKsort($expected); $actual = $this->serializer->decode((string) $response->getBody(), static::$format); - ksort($actual); + $this->nestedKsort($actual); $this->assertSame($expected, $actual); } @@ -1215,4 +1215,19 @@ protected function assertResourceNotAvailable(Url $url, array $request_options) } } + /** + * Sorts a nested array with ksort(). + * + * @param $array + * The nested array to sort. + */ + public static function nestedKsort(&$array) { + ksort($array); + foreach ($array as &$item) { + if (is_array($item)) { + static::nestedKsort($item); + } + } + } + } diff --git a/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestTextItemNormalizerTest.php b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestTextItemNormalizerTest.php new file mode 100644 index 0000000000..c563b5c61a --- /dev/null +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityTest/EntityTestTextItemNormalizerTest.php @@ -0,0 +1,81 @@ + 'Cádiz is the oldest continuously inhabited city in Spain and a nice place to spend a Sunday with friends.', + 'format' => 'plain_text', + 'process_result' => '

Cádiz is the oldest continuously inhabited city in Spain and a nice place to spend a Sunday with friends.

' . "\n", + ], + ]; + return $expected; + } + + /** + * {@inheritdoc} + */ + protected function createEntity() { + $entity = parent::createEntity(); + $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' => 'plain_text', + ]; + $entity->save(); + return $entity; + } + + /** + * {@inheritdoc} + */ + protected function getNormalizedPostEntity() { + $post_entity = parent::getNormalizedPostEntity(); + $post_entity['field_test_text'] = [ + [ + 'value' => 'Llamas are awesome.', + 'format' => 'plain_text', + ], + ]; + return $post_entity; + } + + /** + * {@inheritdoc} + */ + protected function getExpectedCacheTags() { + return Cache::mergeTags(['config:filter.format.plain_text'], parent::getExpectedCacheTags()); + } + + /** + * {@inheritdoc} + */ + protected function getExpectedCacheContexts() { + return Cache::mergeContexts(['languages:language_interface', 'theme'], parent::getExpectedCacheContexts()); + } + +} diff --git a/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php index 698451b58c..4d6b3008a5 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/Term/TermResourceTestBase.php @@ -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\EntityResource\EntityResourceTestBase; @@ -64,6 +65,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 the seven.") ->setChangedTime(123456789); $term->save(); @@ -93,8 +95,9 @@ protected function getExpectedNormalizedEntity() { ], 'description' => [ [ - 'value' => NULL, + 'value' => 'It is a little known fact that llamas cannot count higher the seven.', 'format' => NULL, + 'process_result' => "

It is a little known fact that llamas cannot count higher the seven.

\n", ], ], 'parent' => [], @@ -159,4 +162,18 @@ protected function getExpectedUnauthorizedAccessMessage($method) { } } + /** + * {@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']); + } + } diff --git a/core/modules/serialization/src/Normalizer/ComputedPropertiesNormalizerTrait.php b/core/modules/serialization/src/Normalizer/ComputedPropertiesNormalizerTrait.php new file mode 100644 index 0000000000..e3e52bfce6 --- /dev/null +++ b/core/modules/serialization/src/Normalizer/ComputedPropertiesNormalizerTrait.php @@ -0,0 +1,52 @@ +getProperties(TRUE); + foreach ($properties as $property_name => $property) { + $data_definition = $property->getDataDefinition(); + if ($data_definition->isComputed() && $data_definition instanceof ExposableDataDefinitionInterface) { + if ($data_definition->isExposed()) { + $property_value = $field_item->get($property_name); + $attribute = $this->serializer->normalize($property_value, $format, $context); + + if ($attribute instanceof CacheableDependencyInterface && isset($context['cacheability'])) { + $context['cacheability']->addCacheableDependency($attribute); + + } + if (is_object($attribute) && method_exists($attribute, '__toString')) { + $attribute = (string) $attribute; + } + $attributes[$property_name] = $attribute; + } + } + } + + return $attributes; + } + +} diff --git a/core/modules/serialization/src/Normalizer/FieldItemNormalizer.php b/core/modules/serialization/src/Normalizer/FieldItemNormalizer.php index decca43227..0f8dbfe982 100644 --- a/core/modules/serialization/src/Normalizer/FieldItemNormalizer.php +++ b/core/modules/serialization/src/Normalizer/FieldItemNormalizer.php @@ -11,6 +11,8 @@ */ class FieldItemNormalizer extends ComplexDataNormalizer implements DenormalizerInterface { + use ComputedPropertiesNormalizerTrait; + /** * {@inheritdoc} */ @@ -54,4 +56,18 @@ protected function constructValue($data, $context) { return $data; } + /** + * {@inheritdoc} + */ + public function normalize($object, $format = NULL, array $context = []) { + $attributes = []; + /** @var \Drupal\Core\Field\FieldItemInterface $object */ + foreach ($object as $name => $field) { + $attributes[$name] = $this->serializer->normalize($field, $format, $context); + } + + $attributes = array_merge($attributes, $this->getComputedAttributes($object, $format, $context)); + return $attributes; + } + } diff --git a/core/modules/serialization/tests/modules/field_normalization_test/field_normalization_test.services.yml b/core/modules/serialization/tests/modules/field_normalization_test/field_normalization_test.services.yml index 36243e7954..51fdd4a5fa 100644 --- a/core/modules/serialization/tests/modules/field_normalization_test/field_normalization_test.services.yml +++ b/core/modules/serialization/tests/modules/field_normalization_test/field_normalization_test.services.yml @@ -2,5 +2,5 @@ services: serializer.normalizer.silly_fielditem: class: Drupal\field_normalization_test\Normalization\TextItemSillyNormalizer tags: - # The priority must be higher than serialization.normalizer.field_item. - - { name: normalizer , priority: 9 } + # The priority must be higher than serializer.normalizer.text_item_base. + - { name: normalizer , priority: 30 } diff --git a/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php b/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php index 71fa6b3ec5..094a8771d6 100644 --- a/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php +++ b/core/modules/serialization/tests/src/Kernel/EntitySerializationTest.php @@ -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; /** * Tests that entities can be serialized to supported core formats. @@ -60,6 +62,29 @@ protected function setUp() { // User create needs sequence table. $this->installSchema('system', ['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 +94,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); @@ -128,6 +154,7 @@ public function testNormalize() { [ 'value' => $this->values['field_test_text']['value'], 'format' => $this->values['field_test_text']['format'], + 'process_result' => "

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

", ], ], ]; @@ -168,7 +195,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.'); @@ -193,7 +220,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/Plugin/Field/FieldType/TextItemBase.php b/core/modules/text/src/Plugin/Field/FieldType/TextItemBase.php index 6dd4339299..06e3be4f21 100644 --- a/core/modules/text/src/Plugin/Field/FieldType/TextItemBase.php +++ b/core/modules/text/src/Plugin/Field/FieldType/TextItemBase.php @@ -7,6 +7,8 @@ use Drupal\Core\Field\FieldItemBase; use Drupal\Core\Field\FieldStorageDefinitionInterface; use Drupal\Core\TypedData\DataDefinition; +use Drupal\text\TextProcessed; +use Drupal\text\TextProcessedResult; /** * Base class for 'text' configurable field types. @@ -31,6 +33,14 @@ public static function propertyDefinitions(FieldStorageDefinitionInterface $fiel ->setClass('\Drupal\text\TextProcessed') ->setSetting('text source', 'value'); + $properties['process_result'] = DataDefinition::create('string') + ->setLabel(t('Processed text (object)')) + ->setDescription(t('The text with the text format applied.')) + ->setComputed(TRUE) + ->setClass(TextProcessedResult::class) + ->setSetting('text source', 'value') + ->setExposed(TRUE); + return $properties; } @@ -57,7 +67,7 @@ public function isEmpty() { public function onChange($property_name, $notify = TRUE) { // Unset processed properties that are affected by the change. foreach ($this->definition->getPropertyDefinitions() as $property => $definition) { - if ($definition->getClass() == '\Drupal\text\TextProcessed') { + if (in_array($definition->getClass(), [TextProcessed::class, TextProcessedResult::class], TRUE)) { if ($property_name == 'format' || ($definition->getSetting('text source') == $property_name)) { $this->writePropertyValue($property, NULL); } diff --git a/core/modules/text/src/TextProcessed.php b/core/modules/text/src/TextProcessed.php index 61bef37dd5..dd9c185952 100644 --- a/core/modules/text/src/TextProcessed.php +++ b/core/modules/text/src/TextProcessed.php @@ -2,9 +2,8 @@ namespace Drupal\text; -use Drupal\Core\TypedData\DataDefinitionInterface; -use Drupal\Core\TypedData\TypedDataInterface; -use Drupal\Core\TypedData\TypedData; +use Drupal\Core\Render\Markup; +use Drupal\filter\FilterProcessResult; /** * A computed property for processing text with a format. @@ -12,56 +11,18 @@ * Required settings (below the definition's 'settings' key) are: * - text source: The text property containing the to be processed text. */ -class TextProcessed extends TypedData { - - /** - * Cached processed text. - * - * @var string|null - */ - protected $processed = NULL; - - /** - * {@inheritdoc} - */ - public function __construct(DataDefinitionInterface $definition, $name = NULL, TypedDataInterface $parent = NULL) { - parent::__construct($definition, $name, $parent); - - if ($definition->getSetting('text source') === NULL) { - throw new \InvalidArgumentException("The definition's 'text source' key has to specify the name of the text property to be processed."); - } - } +class TextProcessed extends TextProcessedResult { /** * {@inheritdoc} */ public function getValue() { - if ($this->processed !== NULL) { - return $this->processed; - } - - $item = $this->getParent(); - $text = $item->{($this->definition->getSetting('text source'))}; + $value = parent::getValue(); - // Avoid running check_markup() on empty strings. - if (!isset($text) || $text === '') { - $this->processed = ''; - } - else { - $this->processed = check_markup($text, $item->format, $item->getLangcode()); - } - return $this->processed; - } - - /** - * {@inheritdoc} - */ - public function setValue($value, $notify = TRUE) { - $this->processed = $value; - // Notify the parent of any changes. - if ($notify && isset($this->parent)) { - $this->parent->onChange($this->name); + if ($value !== '' || ($value instanceof FilterProcessResult && $value->getProcessedText() !== '')) { + $value = Markup::create((string) $value); } + return $value; } } diff --git a/core/modules/text/src/TextProcessedResult.php b/core/modules/text/src/TextProcessedResult.php new file mode 100644 index 0000000000..a20d670bff --- /dev/null +++ b/core/modules/text/src/TextProcessedResult.php @@ -0,0 +1,92 @@ +getSetting('text source') === NULL) { + throw new \InvalidArgumentException("The definition's 'text source' key has to specify the name of the text property to be processed."); + } + } + + /** + * {@inheritdoc} + */ + public function getValue() { + if ($this->processed !== NULL) { + return $this->processed; + } + + /** @var \Drupal\Core\Field\FieldItemInterface $item */ + $item = $this->getParent(); + $text = $item->{($this->definition->getSetting('text source'))}; + + // Avoid running \Drupal\Core\Render\RendererInterface::renderPlain + // on empty strings. + if (!isset($text) || $text === '') { + $this->processed = ''; + } + else { + $build = [ + '#type' => 'processed_text', + '#text' => $text, + '#format' => $item->format, + '#filter_types_to_skip' => [], + '#langcode' => $item->getLangcode(), + ]; + // It's necessary to capture the cacheability metadata associated with the + // processed text. See https://www.drupal.org/node/2278483. + $processed_text = $this->getRenderer()->renderPlain($build); + $this->processed = FilterProcessResult::createFromRenderArray($build)->setProcessedText((string) $processed_text); + } + return $this->processed; + } + + /** + * {@inheritdoc} + */ + public function setValue($value, $notify = TRUE) { + $this->processed = $value; + // Notify the parent of any changes. + if ($notify && isset($this->parent)) { + $this->parent->onChange($this->name); + } + } + + /** + * Returns the renderer service. + * + * @return \Drupal\Core\Render\RendererInterface + */ + protected function getRenderer() { + return \Drupal::service('renderer'); + } + +} 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 0000000000..a9bb960ced --- /dev/null +++ b/core/modules/text/tests/src/Kernel/Normalizer/TextItemBaseNormalizerTest.php @@ -0,0 +1,176 @@ +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; + $cacheable_metadata = new CacheableMetadata(); + $data = $this->serializer->normalize($entity, 'json', ['cacheability' => $cacheable_metadata]); + + $expected_cacheability = new CacheableMetadata(); + $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, $cacheable_metadata); + $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; + $cacheable_metadata = new CacheableMetadata(); + $data = $this->serializer->normalize($entity, 'json', ['cacheability' => $cacheable_metadata]); + + $this->assertEquals($expected_cacheability, $cacheable_metadata); + $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/tests/src/Kernel/TextWithSummaryItemTest.php b/core/modules/text/tests/src/Kernel/TextWithSummaryItemTest.php index a48c191b5e..fa9db4018d 100644 --- a/core/modules/text/tests/src/Kernel/TextWithSummaryItemTest.php +++ b/core/modules/text/tests/src/Kernel/TextWithSummaryItemTest.php @@ -81,6 +81,8 @@ public function testCrudAndUpdate() { // Change the format, this should update the processed properties. $entity->summary_field->format = 'no_filters'; + $entity->save(); + $entity = $storage->load($entity->id()); $this->assertEqual($entity->summary_field->processed, $value); $this->assertEqual($entity->summary_field->summary_processed, $summary);