diff --git a/core/modules/filter/src/Element/ProcessedText.php b/core/modules/filter/src/Element/ProcessedText.php index 6e2b3ec43e..b81e0c5629 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 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/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/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php b/core/modules/rest/tests/src/Functional/EntityResource/Comment/CommentResourceTestBase.php index 7b65c848b7..d3de35e771 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\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', + 'process_result' => '

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

' . "\n", ], ], ]; @@ -249,6 +251,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 277c47042b..6cdcf2bf2d 100644 --- a/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php +++ b/core/modules/rest/tests/src/Functional/EntityResource/EntityResourceTestBase.php @@ -465,7 +465,7 @@ public function testGet() { // for the keys with the array order the same (it needs to match with // identical comparison). $expected = $this->getExpectedNormalizedEntity(); - static::recursiveKSort($expected); + static::recursiveKSort($actual); $actual = $this->serializer->decode((string) $response->getBody(), static::$format); static::recursiveKSort($actual); $this->assertSame($expected, $actual); @@ -530,7 +530,7 @@ public function testGet() { // Config entities are not affected. // @see \Drupal\serialization\Normalizer\ConfigEntityNormalizer::normalize() $expected = static::castToString($expected); - static::recursiveKSort($expected); + static::recursiveKSort($actual); $actual = $this->serializer->decode((string) $response->getBody(), static::$format); static::recursiveKSort($actual); $this->assertSame($expected, $actual); 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 cdf446823e..5d5d030c38 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\BcTimestampNormalizerUnixTestTrait; @@ -71,6 +72,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) ->set('path', '/llama'); $term->save(); @@ -101,8 +103,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' => [], @@ -172,4 +175,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/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 ebd4e03317..ac676fc0f0 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; use Drupal\Tests\rest\Functional\BcTimestampNormalizerUnixTestTrait; /** @@ -63,6 +65,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', @@ -72,12 +97,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); @@ -131,6 +157,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']}

", ], ], ]; @@ -171,7 +198,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.'); @@ -198,7 +225,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/TextProcessed.php b/core/modules/text/src/TextProcessedResult.php similarity index 55% copy from core/modules/text/src/TextProcessed.php copy to core/modules/text/src/TextProcessedResult.php index 61bef37dd5..a20d670bff 100644 --- a/core/modules/text/src/TextProcessed.php +++ b/core/modules/text/src/TextProcessedResult.php @@ -5,19 +5,23 @@ use Drupal\Core\TypedData\DataDefinitionInterface; use Drupal\Core\TypedData\TypedDataInterface; use Drupal\Core\TypedData\TypedData; +use Drupal\filter\FilterProcessResult; /** * A computed property for processing text with a format. * * Required settings (below the definition's 'settings' key) are: * - text source: The text property containing the to be processed text. + * + * @deprecated in Drupal 8.3.x and will be removed before Drupal 9.0.0. + * This functionality will be moved into \Drupal\text\TextProcessed */ -class TextProcessed extends TypedData { +class TextProcessedResult extends TypedData { /** * Cached processed text. * - * @var string|null + * @var \Drupal\filter\FilterProcessResult|string|null */ protected $processed = NULL; @@ -40,15 +44,27 @@ public function getValue() { return $this->processed; } + /** @var \Drupal\Core\Field\FieldItemInterface $item */ $item = $this->getParent(); $text = $item->{($this->definition->getSetting('text source'))}; - // Avoid running check_markup() on empty strings. + // Avoid running \Drupal\Core\Render\RendererInterface::renderPlain + // on empty strings. if (!isset($text) || $text === '') { $this->processed = ''; } 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(), + ]; + // 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; } @@ -64,4 +80,13 @@ public function setValue($value, $notify = TRUE) { } } + /** + * 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/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);