diff options
author | Alex Pott | 2014-06-08 21:25:15 (GMT) |
---|---|---|
committer | Alex Pott | 2014-06-08 21:25:15 (GMT) |
commit | d5be5c04543803aeffc2809d31c615760975d6c7 (patch) | |
tree | bd795839b560c427942862fe5a6a23cc4a9ed2ff | |
parent | e05d622e0192044b993f48a39d257a815785c505 (diff) |
Issue #2217877 by Wim Leers, Dave Reid, damiankloip, xjm, chx | catch: Fixed Text filters should be able to add #attached, #post_render_cache, and cache tags.
51 files changed, 1372 insertions, 408 deletions
diff --git a/core/includes/common.inc b/core/includes/common.inc index 5572e54..188268c 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -3468,7 +3468,6 @@ function drupal_render(&$elements, $is_recursive_call = FALSE) { * * @return string * The rendered HTML of all children of the element. - * @see drupal_render() */ function drupal_render_children(&$element, $children_keys = NULL) { @@ -3662,7 +3661,7 @@ function drupal_render_cache_set(&$markup, array $elements) { * placeholders, but should also be called by #post_render_cache callbacks that * want to replace the placeholder with the final markup. * - * @param callable $callback + * @param string $callback * The #post_render_cache callback that will replace the placeholder with its * eventual markup. * @param array $context @@ -3681,7 +3680,7 @@ function drupal_render_cache_generate_placeholder($callback, array &$context) { 'token' => \Drupal\Component\Utility\Crypt::randomBytesBase64(55), ); - return '<drupal:render-cache-placeholder callback="' . $callback . '" token="' . $context['token'] . '" />'; + return '<drupal-render-cache-placeholder callback="' . $callback . '" token="' . $context['token'] . '"></drupal-render-cache-placeholder>'; } /** diff --git a/core/modules/ckeditor/src/Plugin/CKEditorPlugin/Internal.php b/core/modules/ckeditor/src/Plugin/CKEditorPlugin/Internal.php index 7272357..02049b6 100644 --- a/core/modules/ckeditor/src/Plugin/CKEditorPlugin/Internal.php +++ b/core/modules/ckeditor/src/Plugin/CKEditorPlugin/Internal.php @@ -9,8 +9,12 @@ namespace Drupal\ckeditor\Plugin\CKEditorPlugin; use Drupal\ckeditor\CKEditorPluginBase; use Drupal\Component\Utility\NestedArray; +use Drupal\Core\Cache\Cache; +use Drupal\Core\Cache\CacheBackendInterface; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; use Drupal\editor\Entity\Editor; use Drupal\filter\Plugin\FilterInterface; +use Symfony\Component\DependencyInjection\ContainerInterface; /** * Defines the "internal" plugin (i.e. core plugins part of our CKEditor build). @@ -20,7 +24,55 @@ use Drupal\filter\Plugin\FilterInterface; * label = @Translation("CKEditor core") * ) */ -class Internal extends CKEditorPluginBase { +class Internal extends CKEditorPluginBase implements ContainerFactoryPluginInterface { + + /** + * The cache backend. + * + * @var \Drupal\Core\Cache\CacheBackendInterface + */ + protected $cache; + + /** + * Constructs a \Drupal\ckeditor\Plugin\CKEditorPlugin\Internal object. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin_id for the plugin instance. + * @param mixed $plugin_definition + * The plugin implementation definition. + * @param \Drupal\Core\Cache\CacheBackendInterface $cache_backend + * The cache backend. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, CacheBackendInterface $cache_backend) { + $this->cache = $cache_backend; + parent::__construct($configuration, $plugin_id, $plugin_definition); + } + + /** + * Creates an instance of the plugin. + * + * @param \Symfony\Component\DependencyInjection\ContainerInterface $container + * The container to pull out services used in the plugin. + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin ID for the plugin instance. + * @param mixed $plugin_definition + * The plugin implementation definition. + * + * @return static + * Returns an instance of this plugin. + */ + public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('cache.default') + ); + } /** * Implements \Drupal\ckeditor\Plugin\CKEditorPluginInterface::isInternal(). @@ -251,22 +303,42 @@ class Internal extends CKEditorPluginBase { * An array containing the "format_tags" configuration. */ protected function generateFormatTagsSetting(Editor $editor) { - // The <p> tag is always allowed — HTML without <p> tags is nonsensical. - $format_tags = array('p'); + // When no text format is associated yet, assume no tag is allowed. + // @see \Drupal\Editor\EditorInterface::hasAssociatedFilterFormat() + if (!$editor->hasAssociatedFilterFormat()) { + return array(); + } + + $format = $editor->getFilterFormat(); + $cid = 'ckeditor_internal_format_tags:' . $format->id(); + + if ($cached = $this->cache->get($cid)) { + $format_tags = $cached->data; + } + else { + // The <p> tag is always allowed — HTML without <p> tags is nonsensical. + $format_tags = array('p'); - // Given the list of possible format tags, automatically determine whether - // the current text format allows this tag, and thus whether it should show - // up in the "Format" dropdown. - $possible_format_tags = array('h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'pre'); - foreach ($possible_format_tags as $tag) { - $input = '<' . $tag . '>TEST</' . $tag . '>'; - $output = trim(check_markup($input, $editor->id(), '', TRUE)); - if ($input == $output) { - $format_tags[] = $tag; + // Given the list of possible format tags, automatically determine whether + // the current text format allows this tag, and thus whether it should show + // up in the "Format" dropdown. + $possible_format_tags = array('h1', 'h2', 'h3', 'h4', 'h5', 'h6', 'pre'); + foreach ($possible_format_tags as $tag) { + $input = '<' . $tag . '>TEST</' . $tag . '>'; + $output = trim(check_markup($input, $editor->id())); + if ($input == $output) { + $format_tags[] = $tag; + } } + $format_tags = implode(';', $format_tags); + + // Cache the "format_tags" configuration. This cache item is infinitely + // valid; it only changes whenever the text format is changed, hence it's + // tagged with the text format's cache tag. + $this->cache->set($cid, $format_tags, Cache::PERMANENT, $format->getCacheTag()); } - return implode(';', $format_tags); + return $format_tags; } /** diff --git a/core/modules/comment/comment.module b/core/modules/comment/comment.module index dc59c3c..fce19d9 100644 --- a/core/modules/comment/comment.module +++ b/core/modules/comment/comment.module @@ -1399,8 +1399,9 @@ function template_preprocess_comment(&$variables) { $variables['user_picture'] = array(); } - if (\Drupal::config('user.settings')->get('signatures') && $account->getSignature()) { - $variables['signature'] = check_markup($account->getSignature(), $account->getSignatureFormat(), '', TRUE) ; + if (isset($variables['elements']['signature'])) { + $variables['signature'] = $variables['elements']['signature']['#markup']; + unset($variables['elements']['signature']); } else { $variables['signature'] = ''; diff --git a/core/modules/comment/src/CommentViewBuilder.php b/core/modules/comment/src/CommentViewBuilder.php index e993e7a..7769b5e 100644 --- a/core/modules/comment/src/CommentViewBuilder.php +++ b/core/modules/comment/src/CommentViewBuilder.php @@ -131,6 +131,21 @@ class CommentViewBuilder extends EntityViewBuilder { '#markup' => $placeholder, ); + $account = comment_prepare_author($entity); + if (\Drupal::config('user.settings')->get('signatures') && $account->getSignature()) { + $build[$id]['signature'] = array( + '#type' => 'processed_text', + '#text' => $account->getSignature(), + '#format' => $account->getSignatureFormat(), + '#langcode' => $entity->language()->getId(), + ); + // The signature will only be rendered in the theme layer, which means + // its associated cache tags will not bubble up. Work around this for + // now by already rendering the signature here. + // @todo remove this work-around, see https://drupal.org/node/2273277 + drupal_render($build[$id]['signature'], TRUE); + } + if (!isset($build[$id]['#attached'])) { $build[$id]['#attached'] = array(); } diff --git a/core/modules/editor/src/EditorController.php b/core/modules/editor/src/EditorController.php index 59c1ef9..e5a768e 100644 --- a/core/modules/editor/src/EditorController.php +++ b/core/modules/editor/src/EditorController.php @@ -47,7 +47,7 @@ class EditorController extends ControllerBase { // Direct text editing is only supported for single-valued fields. $field = $entity->getTranslation($langcode)->$field_name; - $editable_text = check_markup($field->value, $field->format, $langcode, FALSE, array(FilterInterface::TYPE_TRANSFORM_REVERSIBLE, FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE)); + $editable_text = check_markup($field->value, $field->format, $langcode, array(FilterInterface::TYPE_TRANSFORM_REVERSIBLE, FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE)); $response->addCommand(new GetUntransformedTextCommand($editable_text)); return $response; diff --git a/core/modules/editor/src/Plugin/Filter/EditorFileReference.php b/core/modules/editor/src/Plugin/Filter/EditorFileReference.php new file mode 100644 index 0000000..b22bda6 --- /dev/null +++ b/core/modules/editor/src/Plugin/Filter/EditorFileReference.php @@ -0,0 +1,94 @@ +<?php + +/** + * @file + * Contains \Drupal\editor\Plugin\Filter\EditorFileReference. + */ + +namespace Drupal\editor\Plugin\Filter; + +use Drupal\Component\Utility\Html; +use Drupal\Core\Entity\EntityManagerInterface; +use Drupal\Core\Plugin\ContainerFactoryPluginInterface; +use Drupal\filter\FilterProcessResult; +use Drupal\filter\Plugin\FilterBase; +use Symfony\Component\DependencyInjection\ContainerInterface; + +/** + * Provides a filter to track images uploaded via a Text Editor. + * + * Passes the text unchanged, but associates the cache tags of referenced files. + * + * @Filter( + * id = "editor_file_reference", + * title = @Translation("Track images uploaded via a Text Editor"), + * description = @Translation("Ensures that the latest versions of images uploaded via a Text Editor are displayed."), + * type = Drupal\filter\Plugin\FilterInterface::TYPE_TRANSFORM_REVERSIBLE + * ) + */ +class EditorFileReference extends FilterBase implements ContainerFactoryPluginInterface { + + /** + * An entity manager object. + * + * @var \Drupal\Core\Entity\EntityManagerInterface + */ + protected $entityManager; + + /** + * Constructs a \Drupal\editor\Plugin\Filter\EditorFileReference object. + * + * @param array $configuration + * A configuration array containing information about the plugin instance. + * @param string $plugin_id + * The plugin_id for the plugin instance. + * @param mixed $plugin_definition + * The plugin implementation definition. + * @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager + * An entity manager object. + */ + public function __construct(array $configuration, $plugin_id, $plugin_definition, EntityManagerInterface $entity_manager) { + $this->entityManager = $entity_manager; + parent::__construct($configuration, $plugin_id, $plugin_definition); + } + + /** + * {@inheritdoc} + */ + static public function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) { + return new static( + $configuration, + $plugin_id, + $plugin_definition, + $container->get('entity.manager') + ); + } + + /** + * {@inheritdoc} + */ + public function process($text, $langcode) { + $result = new FilterProcessResult($text); + + if (stristr($text, 'data-editor-file-uuid') !== FALSE) { + $dom = Html::load($text); + $xpath = new \DOMXPath($dom); + $processed_uuids = array(); + foreach ($xpath->query('//*[@data-editor-file-uuid]') as $node) { + $uuid = $node->getAttribute('data-editor-file-uuid'); + // Only process the first occurrence of each file UUID. + if (!isset($processed_uuids[$uuid])) { + $processed_uuids[$uuid] = TRUE; + + $file = $this->entityManager->loadEntityByUuid('file', $uuid); + if ($file) { + $result->addCacheTags($file->getCacheTag()); + } + } + } + } + + return $result; + } + +} diff --git a/core/modules/editor/src/Tests/EditorFileReferenceFilterTest.php b/core/modules/editor/src/Tests/EditorFileReferenceFilterTest.php new file mode 100644 index 0000000..3474613 --- /dev/null +++ b/core/modules/editor/src/Tests/EditorFileReferenceFilterTest.php @@ -0,0 +1,121 @@ +<?php + +/** + * @file + * Contains \Drupal\editor\Tests\EditorFileReferenceFilterTest. + */ + +namespace Drupal\editor\Tests; + +use Drupal\simpletest\KernelTestBase; +use Drupal\filter\FilterBag; + +/** + * Tests for the EditorFileReference filter. + */ +class EditorFileReferenceFilterTest extends KernelTestBase { + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = array('system', 'filter', 'editor', 'field', 'file', 'user'); + + /** + * @var \Drupal\filter\Plugin\FilterInterface[] + */ + protected $filters; + + /** + * {@inheritdoc} + */ + public static function getInfo() { + return array( + 'name' => 'Editor File Reference filter', + 'description' => "Tests Editor module's file reference filter.", + 'group' => 'Text Editor', + ); + } + + /** + * {@inheritdoc} + */ + protected function setUp() { + parent::setUp(); + $this->installConfig(array('system')); + $this->installEntitySchema('file'); + $this->installSchema('file', array('file_usage')); + + $manager = $this->container->get('plugin.manager.filter'); + $bag = new FilterBag($manager, array()); + $this->filters = $bag->getAll(); + } + + /** + * Tests the editor file reference filter. + */ + function testEditorFileReferenceFilter() { + $filter = $this->filters['editor_file_reference']; + + $test = function($input) use ($filter) { + return $filter->process($input, 'und'); + }; + + file_put_contents('public://llama.jpg', $this->randomName()); + $image = entity_create('file', array('uri' => 'public://llama.jpg')); + $image->save(); + $id = $image->id(); + $uuid = $image->uuid(); + + file_put_contents('public://alpaca.jpg', $this->randomName()); + $image_2 = entity_create('file', array('uri' => 'public://alpaca.jpg')); + $image_2->save(); + $id_2 = $image_2->id(); + $uuid_2 = $image_2->uuid(); + + $this->pass('No data-editor-file-uuid attribute.'); + $input = '<img src="llama.jpg" />'; + $output = $test($input); + $this->assertIdentical($input, $output->getProcessedText()); + + $this->pass('One data-editor-file-uuid attribute.'); + $input = '<img src="llama.jpg" data-editor-file-uuid="' . $uuid . '" />'; + $output = $test($input); + $this->assertIdentical($input, $output->getProcessedText()); + $this->assertEqual(array('file' => array($id)), $output->getCacheTags()); + + $this->pass('One data-editor-file-uuid attribute with odd capitalization.'); + $input = '<img src="llama.jpg" DATA-editor-file-UUID = "' . $uuid . '" />'; + $output = $test($input); + $this->assertIdentical($input, $output->getProcessedText()); + $this->assertEqual(array('file' => array($id)), $output->getCacheTags()); + + $this->pass('One data-editor-file-uuid attribute on a non-image tag.'); + $input = '<video src="llama.jpg" data-editor-file-uuid="' . $uuid . '" />'; + $output = $test($input); + $this->assertIdentical($input, $output->getProcessedText()); + $this->assertEqual(array('file' => array($id)), $output->getCacheTags()); + + $this->pass('One data-editor-file-uuid attribute with an invalid value.'); + $input = '<img src="llama.jpg" data-editor-file-uuid="invalid-' . $uuid . '" />'; + $output = $test($input); + $this->assertIdentical($input, $output->getProcessedText()); + $this->assertEqual(array(), $output->getCacheTags()); + + $this->pass('Two different data-editor-file-uuid attributes.'); + $input = '<img src="llama.jpg" data-editor-file-uuid="' . $uuid . '" />'; + $input .= '<img src="alpaca.jpg" data-editor-file-uuid="' . $uuid_2 . '" />'; + $output = $test($input); + $this->assertIdentical($input, $output->getProcessedText()); + $this->assertEqual(array('file' => array($id, $id_2)), $output->getCacheTags()); + + $this->pass('Two identical data-editor-file-uuid attributes.'); + $input = '<img src="llama.jpg" data-editor-file-uuid="' . $uuid . '" />'; + $input .= '<img src="llama.jpg" data-editor-file-uuid="' . $uuid . '" />'; + $output = $test($input); + $this->assertIdentical($input, $output->getProcessedText()); + $this->assertEqual(array('file' => array($id)), $output->getCacheTags()); + } + +} diff --git a/core/modules/entity_reference/src/Plugin/Field/FieldFormatter/EntityReferenceEntityFormatter.php b/core/modules/entity_reference/src/Plugin/Field/FieldFormatter/EntityReferenceEntityFormatter.php index 410a256..4065ea2 100644 --- a/core/modules/entity_reference/src/Plugin/Field/FieldFormatter/EntityReferenceEntityFormatter.php +++ b/core/modules/entity_reference/src/Plugin/Field/FieldFormatter/EntityReferenceEntityFormatter.php @@ -106,6 +106,7 @@ class EntityReferenceEntityFormatter extends EntityReferenceFormatterBase { // its cache tags to be bubbled up and included with those of the // main entity when cache tags are collected for a renderable array // in drupal_render(). + // @todo remove this work-around, see https://drupal.org/node/2273277 $referenced_entity_build = entity_view($item->entity, $view_mode, $item->getLangcode()); drupal_render($referenced_entity_build, TRUE); $elements[$delta] = $referenced_entity_build; diff --git a/core/modules/entity_reference/src/Tests/EntityReferenceFormatterTest.php b/core/modules/entity_reference/src/Tests/EntityReferenceFormatterTest.php index 37fd4b1..61d0ee5 100644 --- a/core/modules/entity_reference/src/Tests/EntityReferenceFormatterTest.php +++ b/core/modules/entity_reference/src/Tests/EntityReferenceFormatterTest.php @@ -60,8 +60,6 @@ class EntityReferenceFormatterTest extends EntityUnitTestBase { public function setUp() { parent::setUp(); - $this->installConfig(array('filter')); - entity_reference_create_instance($this->entityType, $this->bundle, $this->fieldName, 'Field test', $this->entityType); // Set up a field, so that the entity that'll be referenced bubbles up a @@ -88,6 +86,11 @@ class EntityReferenceFormatterTest extends EntityUnitTestBase { )) ->save(); + entity_create('filter_format', array( + 'format' => 'full_html', + 'name' => 'Full HTML', + ))->save(); + // Create the entity to be referenced. $this->referencedEntity = entity_create($this->entityType, array('name' => $this->randomName())); $this->referencedEntity->body = array( @@ -180,7 +183,7 @@ class EntityReferenceFormatterTest extends EntityUnitTestBase { $expected_rendered_body_field = '<div class="field field-entity-test--body field-name-body field-type-text field-label-above"> <div class="field-label">Body: </div> <div class="field-items"> - <div class="field-item"></div> + <div class="field-item"><p>Hello, world!</p></div> </div> </div> '; diff --git a/core/modules/filter/config/install/filter.format.plain_text.yml b/core/modules/filter/config/install/filter.format.plain_text.yml index 70abb88..5de8988 100644 --- a/core/modules/filter/config/install/filter.format.plain_text.yml +++ b/core/modules/filter/config/install/filter.format.plain_text.yml @@ -9,7 +9,6 @@ weight: 10 roles: - anonymous - authenticated -cache: true filters: # Escape all HTML. filter_html_escape: diff --git a/core/modules/filter/config/schema/filter.schema.yml b/core/modules/filter/config/schema/filter.schema.yml index 25c3541..6a28b4e 100644 --- a/core/modules/filter/config/schema/filter.schema.yml +++ b/core/modules/filter/config/schema/filter.schema.yml @@ -30,14 +30,17 @@ filter.format.*: sequence: - type: string label: 'Role' - cache: - type: boolean - label: 'Cache' filters: type: sequence label: 'Enabled filters' sequence: - type: filter + langcode: + type: string + label: 'Default language' + dependencies: + type: config_dependencies + label: 'Dependencies' filter_settings.*: type: sequence diff --git a/core/modules/filter/filter.module b/core/modules/filter/filter.module index f25fe79..9c140a5 100644 --- a/core/modules/filter/filter.module +++ b/core/modules/filter/filter.module @@ -6,6 +6,7 @@ */ use Drupal\Component\Utility\Html; +use Drupal\Component\Utility\NestedArray; use Drupal\Component\Utility\String; use Drupal\Component\Utility\Xss; use Drupal\Core\Cache\Cache; @@ -98,10 +99,132 @@ function filter_element_info() { '#base_type' => 'textarea', '#theme_wrappers' => array('text_format_wrapper'), ); + $type['processed_text'] = array( + '#text' => '', + '#format' => NULL, + '#filter_types_to_skip' => array(), + '#langcode' => '', + '#pre_render' => array('filter_pre_render_text'), + ); return $type; } /** + * Pre-render callback: Renders a processed text element into #markup. + * + * Runs all the enabled filters on a piece of text. + * + * Note: Because filters can inject JavaScript or execute PHP code, security is + * vital here. When a user supplies a text format, you should validate it using + * $format->access() before accepting/using it. This is normally done in the + * validation stage of the Form API. You should for example never make a + * preview of content in a disallowed format. + * + * @param array $element + * A structured array with the following key-value pairs: + * - #text: containing the text to be filtered + * - #format: containing the machine name of the filter format to be used to + * filter the text. Defaults to the fallback format. See + * filter_fallback_format(). + * - #langcode: the language code of the text to be filtered, e.g. 'en' for + * English. This allows filters to be language-aware so language-specific + * text replacement can be implemented. Defaults to an empty string. + * - #filter_types_to_skip: an array of filter types to skip, or an empty + * array (default) to skip no filter types. All of the format's filters will + * be applied, except for filters of the types that are marked to be skipped. + * FilterInterface::TYPE_HTML_RESTRICTOR is the only type that cannot be + * skipped. + * + * @return array + * The passed-in element with the filtered text in '#markup'. + * + * @ingroup sanitization + */ +function filter_pre_render_text(array $element) { + $format_id = $element['#format']; + $filter_types_to_skip = $element['#filter_types_to_skip']; + $text = $element['#text']; + $langcode = $element['#langcode']; + + if (!isset($format_id)) { + $format_id = filter_fallback_format(); + } + // If the requested text format does not exist, the text cannot be filtered. + /** @var \Drupal\filter\Entity\FilterFormat $format **/ + if (!$format = entity_load('filter_format', $format_id)) { + watchdog('filter', 'Missing text format: %format.', array('%format' => $format_id), WATCHDOG_ALERT); + $element['#markup'] = ''; + return $element; + } + + $filter_must_be_applied = function($filter) use ($filter_types_to_skip) { + $enabled = $filter->status === TRUE; + $type = $filter->getType(); + // Prevent FilterInterface::TYPE_HTML_RESTRICTOR from being skipped. + $filter_type_must_be_applied = $type == FilterInterface::TYPE_HTML_RESTRICTOR || !in_array($type, $filter_types_to_skip); + return $enabled && $filter_type_must_be_applied; + }; + + // Convert all Windows and Mac newlines to a single newline, so filters only + // need to deal with one possibility. + $text = str_replace(array("\r\n", "\r"), "\n", $text); + + // Get a complete list of filters, ordered properly. + /** @var \Drupal\filter\FilterBag $filters **/ + $filters = $format->filters(); + + // Give filters the chance to escape HTML-like data such as code or formulas. + /** @var \Drupal\filter\Plugin\FilterInterface $filter **/ + foreach ($filters as $filter) { + if ($filter_must_be_applied($filter)) { + $text = $filter->prepare($text, $langcode); + } + } + + // Perform filtering. + $all_cache_tags = array(); + $all_assets = array(); + $all_post_render_cache_callbacks = array(); + foreach ($filters as $filter) { + if ($filter_must_be_applied($filter)) { + $result = $filter->process($text, $langcode); + $all_assets[] = $result->getAssets(); + $all_cache_tags[] = $result->getCacheTags(); + $all_post_render_cache_callbacks[] = $result->getPostRenderCacheCallbacks(); + $text = $result->getProcessedText(); + } + } + + // Filtering done, store in #markup. + $element['#markup'] = $text; + + // Collect all cache tags. + if (isset($element['#cache']) && isset($element['#cache']['tags'])) { + // Prepend the original cache tags array. + array_unshift($all_cache_tags, $element['#cache']['tags']); + } + // Prepend the text format's cache tags array. + array_unshift($all_cache_tags, $format->getCacheTag()); + $element['#cache']['tags'] = NestedArray::mergeDeepArray($all_cache_tags); + + // Collect all attached assets. + if (isset($element['#attached'])) { + // Prepend the original attached assets array. + array_unshift($all_assets, $element['#attached']); + } + $element['#attached'] = NestedArray::mergeDeepArray($all_assets); + + // Collect all #post_render_cache callbacks. + if (isset($element['#post_render_cache'])) { + // Prepend the original attached #post_render_cache array. + array_unshift($all_assets, $element['#post_render_cache']); + } + $element['#post_render_cache'] = NestedArray::mergeDeepArray($all_post_render_cache_callbacks); + + return $element; +} + +/** * Implements hook_permission(). */ function filter_permission() { @@ -301,24 +424,6 @@ function filter_fallback_format() { } /** - * Checks if the text in a certain text format is allowed to be cached. - * - * This function can be used to check whether the result of the filtering - * process can be cached. A text format may allow caching depending on the - * filters enabled. - * - * @param string $format_id - * The text format ID to check. - * - * @return bool - * TRUE if the given text format allows caching, FALSE otherwise. - */ -function filter_format_allowcache($format_id) { - $format = $format_id ? entity_load('filter_format', $format_id) : FALSE; - return !empty($format->cache); -} - -/** * Runs all the enabled filters on a piece of text. * * Note: Because filters can inject JavaScript or execute PHP code, security is @@ -327,94 +432,46 @@ function filter_format_allowcache($format_id) { * validation stage of the Form API. You should for example never make a * preview of content in a disallowed format. * - * @param $text + * Note: this function should only be used when filtering text for use elsewhere + * than on a rendered HTML page. If this is part of a HTML page, then a + * renderable array with a #type 'processed_text' element should be used instead + * of this, because that will allow cache tags to be set and bubbled up, assets + * to be loaded and #post_render_cache callbacks to be associated. In other + * words: if you are presenting the filtered text in a HTML page, the only way + * this will be presented correctly, is by using the 'processed_text' element. + * + * @param string $text * The text to be filtered. - * @param $format_id + * @param string|null $format_id * (optional) The machine name of the filter format to be used to filter the * text. Defaults to the fallback format. See filter_fallback_format(). - * @param $langcode + * @param string $langcode * (optional) The language code of the text to be filtered, e.g. 'en' for - * English. This allows filters to be language aware so language specific + * English. This allows filters to be language-aware so language-specific * text replacement can be implemented. Defaults to an empty string. - * @param $cache - * (optional) A Boolean indicating whether to cache the filtered output in the - * {cache_filter} table. The caller may set this to FALSE when the output is - * already cached elsewhere to avoid duplicate cache lookups and storage. - * Defaults to FALSE. * @param array $filter_types_to_skip * (optional) An array of filter types to skip, or an empty array (default) * to skip no filter types. All of the format's filters will be applied, * except for filters of the types that are marked to be skipped. - * FilterInterface::TYPE_HTML_RESTRICTOR is the only type that cannot be skipped. + * FilterInterface::TYPE_HTML_RESTRICTOR is the only type that cannot be + * skipped. * - * @return + * @return string * The filtered text. * + * @see filter_process_text() + * * @ingroup sanitization */ -function check_markup($text, $format_id = NULL, $langcode = '', $cache = FALSE, $filter_types_to_skip = array()) { - if (!isset($format_id)) { - $format_id = filter_fallback_format(); - } - // If the requested text format does not exist, the text cannot be filtered. - if (!$format = entity_load('filter_format', $format_id)) { - watchdog('filter', 'Missing text format: %format.', array('%format' => $format_id), WATCHDOG_ALERT); - return ''; - } - - // When certain filters should be skipped, don't perform caching. - if ($filter_types_to_skip) { - $cache = FALSE; - } - - // Check for a cached version of this piece of text. - $cache = $cache && !empty($format->cache); - $cache_id = ''; - if ($cache) { - $cache_id = $format->format . ':' . $langcode . ':' . hash('sha256', $text); - if ($cached = \Drupal::cache('filter')->get($cache_id)) { - return $cached->data; - } - } - - // Convert all Windows and Mac newlines to a single newline, so filters only - // need to deal with one possibility. - $text = str_replace(array("\r\n", "\r"), "\n", $text); - - // Get a complete list of filters, ordered properly. - $filters = $format->filters(); - - $filter_must_be_applied = function($filter) use ($filter_types_to_skip) { - $enabled = $filter->status === TRUE; - $type = $filter->getType(); - // Prevent FilterInterface::TYPE_HTML_RESTRICTOR from being skipped. - $filter_type_must_be_applied = $type == FilterInterface::TYPE_HTML_RESTRICTOR || !in_array($type, $filter_types_to_skip); - return $enabled && $filter_type_must_be_applied; - }; - - // Give filters the chance to escape HTML-like data such as code or formulas. - foreach ($filters as $filter) { - if ($filter_must_be_applied($filter)) { - $text = $filter->prepare($text, $langcode, $cache, $cache_id); - } - } - - // Perform filtering. - foreach ($filters as $filter) { - if ($filter_must_be_applied($filter)) { - $text = $filter->process($text, $langcode, $cache, $cache_id); - } - } - - // Cache the filtered text. This cache is infinitely valid. It becomes - // obsolete when $text changes (which leads to a new $cache_id). It is - // automatically flushed when the text format is updated. - // @see \Drupal\filter\Entity\FilterFormat::save() - if ($cache) { - \Drupal::cache('filter')->set($cache_id, $text, Cache::PERMANENT, array('filter_format' => $format->id())); - } - - return $text; +function check_markup($text, $format_id = NULL, $langcode = '', $filter_types_to_skip = array()) { + $build = array( + '#type' => 'processed_text', + '#text' => $text, + '#format' => $format_id, + '#filter_types_to_skip' => $filter_types_to_skip, + '#langcode' => $langcode, + ); + return drupal_render($build); } /** @@ -1139,10 +1196,3 @@ function filter_filter_secure_image_alter(&$image) { /** * @} End of "defgroup standard_filters". */ - -/** - * Implements hook_page_build(). - */ -function filter_page_build(&$page) { - $page['#attached']['library'][] = 'filter/caption'; -} diff --git a/core/modules/filter/filter.services.yml b/core/modules/filter/filter.services.yml index dc93fdd..406161a 100644 --- a/core/modules/filter/filter.services.yml +++ b/core/modules/filter/filter.services.yml @@ -1,11 +1,4 @@ services: - cache.filter: - class: Drupal\Core\Cache\CacheBackendInterface - tags: - - { name: cache.bin } - factory_method: get - factory_service: cache_factory - arguments: [filter] plugin.manager.filter: class: Drupal\filter\FilterPluginManager parent: default_plugin_manager diff --git a/core/modules/filter/src/Annotation/Filter.php b/core/modules/filter/src/Annotation/Filter.php index a14b72f..808357c 100644 --- a/core/modules/filter/src/Annotation/Filter.php +++ b/core/modules/filter/src/Annotation/Filter.php @@ -65,16 +65,6 @@ class Filter extends Plugin { public $status = FALSE; /** - * Specifies whether the filtered text can be cached. - * - * Note that setting this to FALSE makes the entire text format not cacheable, - * which may have an impact on the site's overall performance. - * - * @var bool (optional) - */ - public $cache = TRUE; - - /** * The default settings for the filter. * * @var array (optional) diff --git a/core/modules/filter/src/Entity/FilterFormat.php b/core/modules/filter/src/Entity/FilterFormat.php index 6f5f807..9598f77 100644 --- a/core/modules/filter/src/Entity/FilterFormat.php +++ b/core/modules/filter/src/Entity/FilterFormat.php @@ -93,13 +93,6 @@ class FilterFormat extends ConfigEntityBase implements FilterFormatInterface, En protected $roles; /** - * Whether processed text of this format can be cached. - * - * @var bool - */ - public $cache = FALSE; - - /** * Configured filters for this text format. * * An associative array of filters assigned to the text format, keyed by the @@ -207,15 +200,6 @@ class FilterFormat extends ConfigEntityBase implements FilterFormatInterface, En // @todo Do not save disabled filters whose properties are identical to // all default properties. - - // Determine whether the format can be cached. - // @todo This is a derived/computed definition, not configuration. - $this->cache = TRUE; - foreach ($this->filters()->getAll() as $filter) { - if ($filter->status && !$filter->cache) { - $this->cache = FALSE; - } - } } /** diff --git a/core/modules/filter/src/FilterProcessResult.php b/core/modules/filter/src/FilterProcessResult.php new file mode 100644 index 0000000..71edc45 --- /dev/null +++ b/core/modules/filter/src/FilterProcessResult.php @@ -0,0 +1,250 @@ +<?php + +/** + * @file + * Contains \Drupal\filter\FilterProcessResult. + */ + +namespace Drupal\filter; + +use Drupal\Component\Utility\NestedArray; + +/** + * Used to return values from a text filter plugin's processing method. + * + * The typical use case for a text filter plugin's processing method is to just + * apply some filtering to the given text, but for more advanced use cases, + * it may be necessary to also: + * 1. declare asset libraries to be loaded; + * 2. declare cache tags that the filtered text depends upon, so when either of + * those cache tags is invalidated, the filtered text should also be + * invalidated; + * 3. apply uncacheable filtering, for example because it differs per user. + * + * In case a filter needs one or more of these advanced use cases, it can use + * the additional methods available. + * + * The typical use case: + * @code + * public function process($text, $langcode) { + * // Modify $text. + * + * return new FilterProcess($text); + * } + * @endcode + * + * The advanced use cases: + * @code + * public function process($text, $langcode) { + * // Modify $text. + * + * $result = new FilterProcess($text); + * + * // Associate assets to be attached. + * $result->setAssets(array( + * 'library' => array( + * 'filter/caption', + * ), + * )); + * + * // Associate cache tags to be invalidated by. + * $result->setCacheTags($node->getCacheTag()); + * + * return $result; + * } + * @endcode + */ +class FilterProcessResult { + + /** + * The processed text. + * + * @see \Drupal\filter\Plugin\FilterInterface::process() + * + * @var string + */ + protected $processedText; + + /** + * An array of associated assets to be attached. + * + * @see drupal_process_attached() + * + * @var array + */ + protected $assets; + + /** + * The attached cache tags. + * + * @see drupal_render_collect_cache_tags() + * + * @var array + */ + protected $cacheTags; + + /** + * The associated #post_render_cache callbacks. + * + * @see _drupal_render_process_post_render_cache() + * + * @var array + */ + protected $postRenderCacheCallbacks; + + /** + * Constructs a FilterProcessResult object. + * + * @param string $processed_text + * The text as processed by a text filter. + */ + public function __construct($processed_text) { + $this->processedText = $processed_text; + + $this->assets = array(); + $this->cacheTags = array(); + $this->postRenderCacheCallbacks = array(); + } + + /** + * Gets the processed text. + * + * @return string + */ + public function getProcessedText() { + return $this->processedText; + } + + /** + * Gets the processed text. + * + * @return string + */ + public function __toString() { + return $this->getProcessedText(); + } + + /** + * Sets the processed text. + * + * @param string $processed_text + * The text as processed by a text filter. + * + * @return $this + */ + public function setProcessedText($processed_text) { + $this->processedText = $processed_text; + return $this; + } + + /** + * Gets cache tags associated with the processed text. + * + * @return array + */ + public function getCacheTags() { + return $this->cacheTags; + } + + /** + * Adds cache tags associated with the processed text. + * + * @param array $cache_tags + * The cache tags to be added. + * + * @return $this + */ + public function addCacheTags(array $cache_tags) { + $this->cacheTags = drupal_merge_attached($this->cacheTags, $cache_tags); + return $this; + } + + /** + * Sets cache tags associated with the processed text. + * + * @param array $cache_tags + * The cache tags to be associated. + * + * @return $this + */ + public function setCacheTags(array $cache_tags) { + $this->cacheTags = $cache_tags; + return $this; + } + + /** + * Gets assets associated with the processed text. + * + * @return array + */ + public function getAssets() { + return $this->assets; + } + + /** + * Adds assets associated with the processed text. + * + * @param array $assets + * The associated assets to be attached. + * + * @return $this + */ + public function addAssets(array $assets) { + $this->assets = NestedArray::mergeDeep($this->assets, $assets); + return $this; + } + + /** + * Sets assets associated with the processed text. + * + * @param array $assets + * The associated assets to be attached. + * + * @return $this + */ + public function setAssets(array $assets) { + $this->assets = $assets; + return $this; + } + + /** + * Gets #post_render_cache callbacks associated with the processed text. + * + * @return array + */ + public function getPostRenderCacheCallbacks() { + return $this->postRenderCacheCallbacks; + } + + /** + * Adds #post_render_cache callbacks associated with the processed text. + * + * @param string $callback + * The #post_render_cache callback that will replace the placeholder with + * its eventual markup. + * @param array $context + * An array providing context for the #post_render_cache callback. + * + * @see drupal_render_cache_generate_placeholder() + * + * @return $this + */ + public function addPostRenderCacheCallback($callback, array $context) { + $this->postRenderCacheCallbacks[$callback][] = $context; + return $this; + } + + /** + * Sets #post_render_cache callbacks associated with the processed text. + * + * @param array $post_render_cache_callbacks + * The associated #post_render_cache callbacks to be executed. + * + * @return $this + */ + public function setPostRenderCacheCallbacks(array $post_render_cache_callbacks) { + $this->postRenderCacheCallbacks = $post_render_cache_callbacks; + return $this; + } + +} diff --git a/core/modules/filter/src/Plugin/Filter/FilterAutoP.php b/core/modules/filter/src/Plugin/Filter/FilterAutoP.php index a77b19e..0627527 100644 --- a/core/modules/filter/src/Plugin/Filter/FilterAutoP.php +++ b/core/modules/filter/src/Plugin/Filter/FilterAutoP.php @@ -7,6 +7,7 @@ namespace Drupal\filter\Plugin\Filter; +use Drupal\filter\FilterProcessResult; use Drupal\filter\Plugin\FilterBase; /** @@ -23,8 +24,8 @@ class FilterAutoP extends FilterBase { /** * {@inheritdoc} */ - public function process($text, $langcode, $cache, $cache_id) { - return _filter_autop($text); + public function process($text, $langcode) { + return new FilterProcessResult(_filter_autop($text)); } /** diff --git a/core/modules/filter/src/Plugin/Filter/FilterCaption.php b/core/modules/filter/src/Plugin/Filter/FilterCaption.php index 08ad4c8..bccaf02 100644 --- a/core/modules/filter/src/Plugin/Filter/FilterCaption.php +++ b/core/modules/filter/src/Plugin/Filter/FilterCaption.php @@ -11,6 +11,7 @@ use Drupal\Component\Utility\Html; use Drupal\Component\Utility\String; use Drupal\Component\Utility\Unicode; use Drupal\Component\Utility\Xss; +use Drupal\filter\FilterProcessResult; use Drupal\filter\Plugin\FilterBase; /** @@ -28,9 +29,11 @@ class FilterCaption extends FilterBase { /** * {@inheritdoc} */ - public function process($text, $langcode, $cache, $cache_id) { + public function process($text, $langcode) { + $result = new FilterProcessResult($text); if (stristr($text, 'data-caption') !== FALSE || stristr($text, 'data-align') !== FALSE) { + $caption_found = FALSE; $dom = Html::load($text); $xpath = new \DOMXPath($dom); foreach ($xpath->query('//*[@data-caption or @data-align]') as $node) { @@ -71,6 +74,9 @@ class FilterCaption extends FilterBase { } continue; } + else { + $caption_found = TRUE; + } // Given the updated node, caption and alignment: re-render it with a // caption. @@ -96,10 +102,18 @@ class FilterCaption extends FilterBase { $node->parentNode->replaceChild($updated_node, $node); } - return Html::serialize($dom); + $result->setProcessedText(Html::serialize($dom)); + + if ($caption_found) { + $result->addAssets(array( + 'library' => array( + 'filter/caption', + ), + )); + } } - return $text; + return $result; } /** diff --git a/core/modules/filter/src/Plugin/Filter/FilterHtml.php b/core/modules/filter/src/Plugin/Filter/FilterHtml.php index 109d943..543f147 100644 --- a/core/modules/filter/src/Plugin/Filter/FilterHtml.php +++ b/core/modules/filter/src/Plugin/Filter/FilterHtml.php @@ -7,6 +7,7 @@ namespace Drupal\filter\Plugin\Filter; +use Drupal\filter\FilterProcessResult; use Drupal\filter\Plugin\FilterBase; /** @@ -58,8 +59,8 @@ class FilterHtml extends FilterBase { /** * {@inheritdoc} */ - public function process($text, $langcode, $cache, $cache_id) { - return _filter_html($text, $this); + public function process($text, $langcode) { + return new FilterProcessResult(_filter_html($text, $this)); } /** diff --git a/core/modules/filter/src/Plugin/Filter/FilterHtmlCorrector.php b/core/modules/filter/src/Plugin/Filter/FilterHtmlCorrector.php index bc0a036..0be711c 100644 --- a/core/modules/filter/src/Plugin/Filter/FilterHtmlCorrector.php +++ b/core/modules/filter/src/Plugin/Filter/FilterHtmlCorrector.php @@ -8,6 +8,7 @@ namespace Drupal\filter\Plugin\Filter; use Drupal\Component\Utility\Html; +use Drupal\filter\FilterProcessResult; use Drupal\filter\Plugin\FilterBase; /** @@ -25,8 +26,8 @@ class FilterHtmlCorrector extends FilterBase { /** * {@inheritdoc} */ - public function process($text, $langcode, $cache, $cache_id) { - return Html::normalize($text); + public function process($text, $langcode) { + return new FilterProcessResult(Html::normalize($text)); } } diff --git a/core/modules/filter/src/Plugin/Filter/FilterHtmlEscape.php b/core/modules/filter/src/Plugin/Filter/FilterHtmlEscape.php index 0825df0..a508298 100644 --- a/core/modules/filter/src/Plugin/Filter/FilterHtmlEscape.php +++ b/core/modules/filter/src/Plugin/Filter/FilterHtmlEscape.php @@ -7,6 +7,7 @@ namespace Drupal\filter\Plugin\Filter; +use Drupal\filter\FilterProcessResult; use Drupal\filter\Plugin\FilterBase; /** @@ -24,8 +25,8 @@ class FilterHtmlEscape extends FilterBase { /** * {@inheritdoc} */ - public function process($text, $langcode, $cache, $cache_id) { - return _filter_html_escape($text); + public function process($text, $langcode) { + return new FilterProcessResult(_filter_html_escape($text)); } /** diff --git a/core/modules/filter/src/Plugin/Filter/FilterHtmlImageSecure.php b/core/modules/filter/src/Plugin/Filter/FilterHtmlImageSecure.php index f00b055..f03c75a 100644 --- a/core/modules/filter/src/Plugin/Filter/FilterHtmlImageSecure.php +++ b/core/modules/filter/src/Plugin/Filter/FilterHtmlImageSecure.php @@ -7,6 +7,7 @@ namespace Drupal\filter\Plugin\Filter; +use Drupal\filter\FilterProcessResult; use Drupal\filter\Plugin\FilterBase; /** @@ -25,8 +26,8 @@ class FilterHtmlImageSecure extends FilterBase { /** * {@inheritdoc} */ - public function process($text, $langcode, $cache, $cache_id) { - return _filter_html_image_secure_process($text); + public function process($text, $langcode) { + return new FilterProcessResult(_filter_html_image_secure_process($text)); } /** diff --git a/core/modules/filter/src/Plugin/Filter/FilterNull.php b/core/modules/filter/src/Plugin/Filter/FilterNull.php index d4fbe62..9c30edc 100644 --- a/core/modules/filter/src/Plugin/Filter/FilterNull.php +++ b/core/modules/filter/src/Plugin/Filter/FilterNull.php @@ -7,6 +7,7 @@ namespace Drupal\filter\Plugin\Filter; +use Drupal\filter\FilterProcessResult; use Drupal\filter\Plugin\FilterBase; /** @@ -47,8 +48,8 @@ class FilterNull extends FilterBase { /** * {@inheritdoc} */ - public function process($text, $langcode, $cache, $cache_id) { - return ''; + public function process($text, $langcode) { + return new FilterProcessResult(''); } /** diff --git a/core/modules/filter/src/Plugin/Filter/FilterUrl.php b/core/modules/filter/src/Plugin/Filter/FilterUrl.php index 0918b19..469ff41 100644 --- a/core/modules/filter/src/Plugin/Filter/FilterUrl.php +++ b/core/modules/filter/src/Plugin/Filter/FilterUrl.php @@ -7,6 +7,7 @@ namespace Drupal\filter\Plugin\Filter; +use Drupal\filter\FilterProcessResult; use Drupal\filter\Plugin\FilterBase; /** @@ -41,8 +42,8 @@ class FilterUrl extends FilterBase { /** * {@inheritdoc} */ - public function process($text, $langcode, $cache, $cache_id) { - return _filter_url($text, $this); + public function process($text, $langcode) { + return new FilterProcessResult(_filter_url($text, $this)); } /** diff --git a/core/modules/filter/src/Plugin/FilterBase.php b/core/modules/filter/src/Plugin/FilterBase.php index facd818..44421e1 100644 --- a/core/modules/filter/src/Plugin/FilterBase.php +++ b/core/modules/filter/src/Plugin/FilterBase.php @@ -45,13 +45,6 @@ abstract class FilterBase extends PluginBase implements FilterInterface { public $weight = 0; /** - * A Boolean indicating whether the text processed by this filter may be cached. - * - * @var bool - */ - public $cache = TRUE; - - /** * An associative array containing the configured settings of this filter. * * @var array @@ -72,7 +65,6 @@ abstract class FilterBase extends PluginBase implements FilterInterface { parent::__construct($configuration, $plugin_id, $plugin_definition); $this->provider = $this->pluginDefinition['provider']; - $this->cache = $this->pluginDefinition['cache']; $this->setConfiguration($configuration); } @@ -154,7 +146,7 @@ abstract class FilterBase extends PluginBase implements FilterInterface { /** * {@inheritdoc} */ - public function prepare($text, $langcode, $cache, $cache_id) { + public function prepare($text, $langcode) { return $text; } diff --git a/core/modules/filter/src/Plugin/FilterInterface.php b/core/modules/filter/src/Plugin/FilterInterface.php index f6f7dd2..fea8559 100644 --- a/core/modules/filter/src/Plugin/FilterInterface.php +++ b/core/modules/filter/src/Plugin/FilterInterface.php @@ -36,18 +36,19 @@ use Drupal\Component\Plugin\ConfigurablePluginInterface; * should then actually change the content: transform URLs into hyperlinks, * convert smileys into images, etc. * - * For performance reasons, content is only filtered once; the result is stored - * in the cache table and retrieved from the cache the next time the same piece - * of content is displayed. If a filter's output is dynamic, it can override - * the cache mechanism, but obviously this should be used with caution: having - * one filter that does not support caching in a collection of filters disables - * caching for the entire collection, not just for one filter. + * @see filter_process_text() + * @see check_markup() * - * Beware of the filter cache when developing your module: it is advised to set - * your filter to 'cache' to FALSE while developing, but be sure to remove that - * setting if it's not needed, when you are no longer in development mode. + * Typically, only text processing is applied, but in more advanced use cases, + * filters may also: + * - declare asset libraries to be loaded; + * - declare cache tags that the resulting filtered text depends upon, so when + * either of those cache tags is invalidated, the render-cached HTML that the + * filtered text is part of should also be invalidated; + * - declare #post_render_cache callbacks to apply uncacheable filtering, for + * example because it differs per user. * - * @see check_markup() + * @see \Drupal\filter\Plugin\FilterInterface::process() * * Filters are discovered through annotations, which may contain the following * definition properties: @@ -67,9 +68,6 @@ use Drupal\Component\Plugin\ConfigurablePluginInterface; * - status: The default status for new instances of the filter. Defaults to * FALSE. * - weight: A default weight for new instances of the filter. Defaults to 0. - * - cache: Whether the filtered text can be cached. Defaults to TRUE. - * Note that setting this to FALSE disables caching for an entire text format, - * which can have a negative impact on the site's overall performance. * - settings: An associative array containing default settings for new * instances of the filter. * @@ -151,16 +149,11 @@ interface FilterInterface extends ConfigurablePluginInterface, PluginInspectionI * The text string to be filtered. * @param string $langcode * The language code of the text to be filtered. - * @param bool $cache - * A Boolean indicating whether the filtered text is going to be cached in - * {cache_filter}. - * @param string $cache_id - * The ID of the filtered text in {cache_filter}, if $cache is TRUE. * * @return string * The prepared, escaped text. */ - public function prepare($text, $langcode, $cache, $cache_id); + public function prepare($text, $langcode); /** * Performs the filter processing. @@ -169,16 +162,14 @@ interface FilterInterface extends ConfigurablePluginInterface, PluginInspectionI * The text string to be filtered. * @param string $langcode * The language code of the text to be filtered. - * @param bool $cache - * A Boolean indicating whether the filtered text is going to be cached in - * {cache_filter}. - * @param string $cache_id - * The ID of the filtered text in {cache_filter}, if $cache is TRUE. * - * @return string - * The filtered text. + * @return \Drupal\filter\FilterProcessResult + * The filtered text, wrapped in a FilterProcessResult object, and possibly + * with associated assets, cache tags and #post_render_cache callbacks. + * + * @see \Drupal\filter\FilterProcessResult */ - public function process($text, $langcode, $cache, $cache_id); + public function process($text, $langcode); /** * Returns HTML allowed by this filter's configuration. diff --git a/core/modules/filter/src/Tests/FilterAPITest.php b/core/modules/filter/src/Tests/FilterAPITest.php index 1c33d74..e0c06d5 100644 --- a/core/modules/filter/src/Tests/FilterAPITest.php +++ b/core/modules/filter/src/Tests/FilterAPITest.php @@ -106,12 +106,12 @@ class FilterAPITest extends EntityUnitTestBase { $expected_filter_text_without_html_generators = "Text with evil content and a URL: http://drupal.org!"; $this->assertIdentical( - check_markup($text, 'filtered_html', '', FALSE, array()), + check_markup($text, 'filtered_html', '', array()), $expected_filtered_text, 'Expected filter result.' ); $this->assertIdentical( - check_markup($text, 'filtered_html', '', FALSE, array(FilterInterface::TYPE_MARKUP_LANGUAGE)), + check_markup($text, 'filtered_html', '', array(FilterInterface::TYPE_MARKUP_LANGUAGE)), $expected_filter_text_without_html_generators, 'Expected filter result when skipping FilterInterface::TYPE_MARKUP_LANGUAGE filters.' ); @@ -120,7 +120,7 @@ class FilterAPITest extends EntityUnitTestBase { // Drupal core only ships with these two types of filters, so this is the // most extensive test possible. $this->assertIdentical( - check_markup($text, 'filtered_html', '', FALSE, array(FilterInterface::TYPE_HTML_RESTRICTOR, FilterInterface::TYPE_MARKUP_LANGUAGE)), + check_markup($text, 'filtered_html', '', array(FilterInterface::TYPE_HTML_RESTRICTOR, FilterInterface::TYPE_MARKUP_LANGUAGE)), $expected_filter_text_without_html_generators, 'Expected filter result when skipping FilterInterface::TYPE_MARKUP_LANGUAGE filters, even when trying to disable filters of the FilterInterface::TYPE_HTML_RESTRICTOR type.' ); @@ -224,6 +224,73 @@ class FilterAPITest extends EntityUnitTestBase { } /** + * Tests the 'processed_text' element. + * + * check_markup() is a wrapper for the 'processed_text' element, for use in + * simple scenarios; the 'processed_text' element has more advanced features: + * it lets filters attach assets, associate cache tags and define + * #post_render_cache callbacks. + * This test focuses solely on those advanced features. + */ + function testProcessedTextElement() { + entity_create('filter_format', array( + 'format' => 'element_test', + 'name' => 'processed_text element test format', + 'filters' => array( + 'filter_test_assets' => array( + 'weight' => -1, + 'status' => TRUE, + ), + 'filter_test_cache_tags' => array( + 'weight' => 0, + 'status' => TRUE, + ), + 'filter_test_post_render_cache' => array( + 'weight' => 1, + 'status' => TRUE, + ), + // Run the HTML corrector filter last, because it has the potential to + // break the render cache placeholders added by the + // filter_test_post_render_cache filter. + 'filter_htmlcorrector' => array( + 'weight' => 10, + 'status' => TRUE, + ), + ), + ))->save(); + + $build = array( + '#type' => 'processed_text', + '#text' => '<p>Hello, world!</p>', + '#format' => 'element_test', + ); + drupal_render($build); + + // Verify the assets, cache tags and #post_render_cache callbacks. + $expected_assets = array( + // The assets attached by the filter_test_assets filter. + 'library' => array( + 'filter/caption', + ), + ); + $this->assertEqual($expected_assets, $build['#attached'], 'Expected assets present'); + $expected_cache_tags = array( + // The cache tag set by the processed_text element itself. + 'filter_format' => array( + 'element_test' => 'element_test', + ), + // The cache tags set by the filter_test_cache_tags filter. + 'foo' => array( + 'bar' => 'bar', + 'baz' => 'baz', + ), + ); + $this->assertEqual($expected_cache_tags, $build['#cache']['tags'], 'Expected cache tags present.'); + $expected_markup = '<p>Hello, world!</p><p>This is a dynamic llama.</p>'; + $this->assertEqual($expected_markup, $build['#markup'], 'Expected #post_render_cache callback has been applied.'); + } + + /** * Tests the function of the typed data type. */ function testTypedDataAPI() { diff --git a/core/modules/filter/src/Tests/FilterAdminTest.php b/core/modules/filter/src/Tests/FilterAdminTest.php index 7aa30a8..9e6d74b 100644 --- a/core/modules/filter/src/Tests/FilterAdminTest.php +++ b/core/modules/filter/src/Tests/FilterAdminTest.php @@ -8,7 +8,6 @@ namespace Drupal\filter\Tests; use Drupal\simpletest\WebTestBase; -use Drupal\filter\Plugin\FilterInterface; /** * Tests the administrative functionality of the Filter module. @@ -354,73 +353,4 @@ class FilterAdminTest extends WebTestBase { $this->assertNoRaw(t('The text format %format has been updated.', array('%format' => 'Basic HTML'))); } - /** - * Tests that changing filter properties clears the filter cache. - */ - public function testFilterAdminClearsFilterCache() { - $restricted = 'restricted_html'; - $original_markup = '<h4>Small headers</h4> small headers are <em>allowed</em> in restricted html by default'; - - // Check that the filter cache is empty for the test markup. - $cid = $this->computeFilterCacheId($original_markup, $restricted, '', TRUE); - $this->assertFalse(\Drupal::cache('filter')->get($cid)); - - // Check that the filter cache gets populated when check_markup is called. - $actual_markup = check_markup($original_markup, $restricted, '', TRUE); - $this->assertTrue(\Drupal::cache('filter')->get($cid)); - $this->assertIdentical(strpos($actual_markup, '<h4>'), 0, 'The h4 tag is present in the resulting markup'); - - // Edit the restricted filter format. - $edit = array(); - $edit['filters[filter_html][settings][allowed_html]'] = '<a> <em> <strong> <cite> <code>'; - $this->drupalPostForm('admin/config/content/formats/manage/' . $restricted, $edit, t('Save configuration')); - $this->assertUrl('admin/config/content/formats'); - $this->drupalGet('admin/config/content/formats/manage/' . $restricted); - $this->assertFieldByName('filters[filter_html][settings][allowed_html]', $edit['filters[filter_html][settings][allowed_html]'], 'Allowed HTML tag added.'); - - // Check that the filter cache is empty after the format was changed. - $this->assertFalse(\Drupal::cache('filter')->get($cid)); - - // Check that after changind the filter, the changes are reflected in the - // filtered markup. - $actual_markup = check_markup($original_markup, $restricted, '', TRUE); - $this->assertIdentical(strpos($actual_markup, '<h4>'), FALSE, 'The h4 tag is not present in the resulting markup'); - } - - - /** - * Computes the cache-key for the given text just like check_markup(). - * - * Note that this is copied over from check_markup(). - * - * @return string|NULL - * The cache-key used to store the text in the filter cache. - */ - protected function computeFilterCacheId($text, $format_id = NULL, $langcode = '', $cache = FALSE, $filter_types_to_skip = array()) { - if (!isset($format_id)) { - $format_id = filter_fallback_format(); - } - // If the requested text format does not exist, the text cannot be filtered. - if (!$format = entity_load('filter_format', $format_id)) { - return; - } - - // Prevent FilterInterface::TYPE_HTML_RESTRICTOR from being skipped. - if (in_array(FilterInterface::TYPE_HTML_RESTRICTOR, $filter_types_to_skip)) { - $filter_types_to_skip = array_diff($filter_types_to_skip, array(FilterInterface::TYPE_HTML_RESTRICTOR)); - } - - // When certain filters should be skipped, don't perform caching. - if ($filter_types_to_skip) { - $cache = FALSE; - } - - // Compute the cache key if the text is cacheable. - $cache = $cache && !empty($format->cache); - $cache_id = ''; - if ($cache) { - return $format->format . ':' . $langcode . ':' . hash('sha256', $text); - } - } - } diff --git a/core/modules/filter/src/Tests/FilterCrudTest.php b/core/modules/filter/src/Tests/FilterCrudTest.php index 041f309..b592c5f 100644 --- a/core/modules/filter/src/Tests/FilterCrudTest.php +++ b/core/modules/filter/src/Tests/FilterCrudTest.php @@ -65,8 +65,8 @@ class FilterCrudTest extends DrupalUnitTestBase { $format->save(); $this->verifyTextFormat($format); - // Add a uncacheable filter and save again. - $format->setFilterConfig('filter_test_uncacheable', array( + // Add a filter_test_replace filter and save again. + $format->setFilterConfig('filter_test_replace', array( 'status' => 1, )); $format->save(); @@ -90,22 +90,9 @@ class FilterCrudTest extends DrupalUnitTestBase { $filter_format = entity_load('filter_format', $format->format); $this->assertEqual($filter_format->format, $format->format, format_string('filter_format_load: Proper format id for text format %format.', $t_args)); $this->assertEqual($filter_format->name, $format->name, format_string('filter_format_load: Proper title for text format %format.', $t_args)); - $this->assertEqual($filter_format->cache, $format->cache, format_string('filter_format_load: Proper cache indicator for text format %format.', $t_args)); $this->assertEqual($filter_format->weight, $format->weight, format_string('filter_format_load: Proper weight for text format %format.', $t_args)); // Check that the filter was created in site default language. $this->assertEqual($format->langcode, $default_langcode, format_string('filter_format_load: Proper language code for text format %format.', $t_args)); - - // Verify the 'cache' text format property according to enabled filters. - $cacheable = TRUE; - foreach ($format->filters() as $name => $filter) { - // If this filter is not cacheable, update $cacheable accordingly, so we - // can verify $format->cache after iterating over all filters. - if ($filter->status && !$filter->cache) { - $cacheable = FALSE; - break; - } - } - $this->assertEqual($filter_format->cache, $cacheable, 'Text format contains proper cache property.'); } } diff --git a/core/modules/filter/src/Tests/FilterDefaultConfigTest.php b/core/modules/filter/src/Tests/FilterDefaultConfigTest.php index 7423b70..1ff9773 100644 --- a/core/modules/filter/src/Tests/FilterDefaultConfigTest.php +++ b/core/modules/filter/src/Tests/FilterDefaultConfigTest.php @@ -49,7 +49,6 @@ class FilterDefaultConfigTest extends DrupalUnitTestBase { // Verify that format default property values have been added/injected. $this->assertTrue($format->uuid()); - $this->assertEqual($format->get('cache'), 1); // Verify that the loaded format does not contain any roles. $this->assertEqual($format->get('roles'), NULL); diff --git a/core/modules/filter/src/Tests/FilterSecurityTest.php b/core/modules/filter/src/Tests/FilterSecurityTest.php index 6321c1b..6aea5de 100644 --- a/core/modules/filter/src/Tests/FilterSecurityTest.php +++ b/core/modules/filter/src/Tests/FilterSecurityTest.php @@ -103,7 +103,7 @@ class FilterSecurityTest extends WebTestBase { function testSkipSecurityFilters() { $text = "Text with some disallowed tags: <script />, <em><object>unicorn</object></em>, <i><table></i>."; $expected_filtered_text = "Text with some disallowed tags: , <em>unicorn</em>, ."; - $this->assertEqual(check_markup($text, 'filtered_html', '', FALSE, array()), $expected_filtered_text, 'Expected filter result.'); - $this->assertEqual(check_markup($text, 'filtered_html', '', FALSE, array(FilterInterface::TYPE_HTML_RESTRICTOR)), $expected_filtered_text, 'Expected filter result, even when trying to disable filters of the FilterInterface::TYPE_HTML_RESTRICTOR type.'); + $this->assertEqual(check_markup($text, 'filtered_html', '', array()), $expected_filtered_text, 'Expected filter result.'); + $this->assertEqual(check_markup($text, 'filtered_html', '', array(FilterInterface::TYPE_HTML_RESTRICTOR)), $expected_filtered_text, 'Expected filter result, even when trying to disable filters of the FilterInterface::TYPE_HTML_RESTRICTOR type.'); } } diff --git a/core/modules/filter/src/Tests/FilterUnitTest.php b/core/modules/filter/src/Tests/FilterUnitTest.php index a51d6b9..6426bf6 100644 --- a/core/modules/filter/src/Tests/FilterUnitTest.php +++ b/core/modules/filter/src/Tests/FilterUnitTest.php @@ -52,107 +52,141 @@ class FilterUnitTest extends DrupalUnitTestBase { $filter = $this->filters['filter_caption']; $test = function($input) use ($filter) { - return $filter->process($input, 'und', FALSE, ''); + return $filter->process($input, 'und'); }; + $attached_library = array( + 'library' => array( + 'filter/caption', + ), + ); + // No data-caption nor data-align attributes. $input = '<img src="llama.jpg" />'; $expected = $input; - $this->assertIdentical($expected, $test($input)); + $this->assertIdentical($expected, $test($input)->getProcessedText()); // Only data-caption attribute. $input = '<img src="llama.jpg" data-caption="Loquacious llama!" />'; $expected = '<figure class="caption caption-img"><img src="llama.jpg" /><figcaption>Loquacious llama!</figcaption></figure>'; - $this->assertIdentical($expected, $test($input)); + $output = $test($input); + $this->assertIdentical($expected, $output->getProcessedText()); + $this->assertIdentical($attached_library, $output->getAssets()); // Empty data-caption attribute. $input = '<img src="llama.jpg" data-caption="" />'; $expected = '<img src="llama.jpg" />'; - $this->assertIdentical($expected, $test($input)); + $this->assertIdentical($expected, $test($input)->getProcessedText()); // HTML entities in the caption. $input = '<img src="llama.jpg" data-caption="“Loquacious llama!”" />'; $expected = '<figure class="caption caption-img"><img src="llama.jpg" /><figcaption>“Loquacious llama!”</figcaption></figure>'; - $this->assertIdentical($expected, $test($input)); + $output = $test($input); + $this->assertIdentical($expected, $output->getProcessedText()); + $this->assertIdentical($attached_library, $output->getAssets()); // HTML encoded as HTML entities in data-caption attribute. $input = '<img src="llama.jpg" data-caption="<em>Loquacious llama!</em>" />'; $expected = '<figure class="caption caption-img"><img src="llama.jpg" /><figcaption><em>Loquacious llama!</em></figcaption></figure>'; - $this->assertIdentical($expected, $test($input)); + $output = $test($input); + $this->assertIdentical($expected, $output->getProcessedText()); + $this->assertIdentical($attached_library, $output->getAssets()); // HTML (not encoded as HTML entities) in data-caption attribute, which is // not allowed by the HTML spec, but may happen when people manually write // HTML, so we explicitly support it. $input = '<img src="llama.jpg" data-caption="<em>Loquacious llama!</em>" />'; $expected = '<figure class="caption caption-img"><img src="llama.jpg" /><figcaption><em>Loquacious llama!</em></figcaption></figure>'; - $this->assertIdentical($expected, $test($input)); + $output = $test($input); + $this->assertIdentical($expected, $output->getProcessedText()); + $this->assertIdentical($attached_library, $output->getAssets()); // Security test: attempt an XSS. $input = '<img src="llama.jpg" data-caption="<script>alert(\'Loquacious llama!\')</script>" />'; $expected = '<figure class="caption caption-img"><img src="llama.jpg" /><figcaption>alert(\'Loquacious llama!\')</figcaption></figure>'; - $this->assertIdentical($expected, $test($input)); + $output = $test($input); + $this->assertIdentical($expected, $output->getProcessedText()); + $this->assertIdentical($attached_library, $output->getAssets()); // Only data-align attribute: all 3 allowed values. $input = '<img src="llama.jpg" data-align="left" />'; $expected = '<img src="llama.jpg" class="align-left" />'; - $this->assertIdentical($expected, $test($input)); + $this->assertIdentical($expected, $test($input)->getProcessedText()); $input = '<img src="llama.jpg" data-align="center" />'; $expected = '<img src="llama.jpg" class="align-center" />'; - $this->assertIdentical($expected, $test($input)); + $this->assertIdentical($expected, $test($input)->getProcessedText()); $input = '<img src="llama.jpg" data-align="right" />'; $expected = '<img src="llama.jpg" class="align-right" />'; - $this->assertIdentical($expected, $test($input)); + $this->assertIdentical($expected, $test($input)->getProcessedText()); // Only data-align attribute: a disallowed value. $input = '<img src="llama.jpg" data-align="left foobar" />'; $expected = '<img src="llama.jpg" />'; - $this->assertIdentical($expected, $test($input)); + $this->assertIdentical($expected, $test($input)->getProcessedText()); // Empty data-align attribute. $input = '<img src="llama.jpg" data-align="" />'; $expected = '<img src="llama.jpg" />'; - $this->assertIdentical($expected, $test($input)); + $this->assertIdentical($expected, $test($input)->getProcessedText()); // Both data-caption and data-align attributes: all 3 allowed values for the // data-align attribute. $input = '<img src="llama.jpg" data-caption="Loquacious llama!" data-align="left" />'; $expected = '<figure class="caption caption-img align-left"><img src="llama.jpg" /><figcaption>Loquacious llama!</figcaption></figure>'; - $this->assertIdentical($expected, $test($input)); + $output = $test($input); + $this->assertIdentical($expected, $output->getProcessedText()); + $this->assertIdentical($attached_library, $output->getAssets()); $input = '<img src="llama.jpg" data-caption="Loquacious llama!" data-align="center" />'; $expected = '<figure class="caption caption-img align-center"><img src="llama.jpg" /><figcaption>Loquacious llama!</figcaption></figure>'; - $this->assertIdentical($expected, $test($input)); + $output = $test($input); + $this->assertIdentical($expected, $output->getProcessedText()); + $this->assertIdentical($attached_library, $output->getAssets()); $input = '<img src="llama.jpg" data-caption="Loquacious llama!" data-align="right" />'; $expected = '<figure class="caption caption-img align-right"><img src="llama.jpg" /><figcaption>Loquacious llama!</figcaption></figure>'; - $this->assertIdentical($expected, $test($input)); + $output = $test($input); + $this->assertIdentical($expected, $output->getProcessedText()); + $this->assertIdentical($attached_library, $output->getAssets()); // Both data-caption and data-align attributes, but a disallowed data-align // attribute value. $input = '<img src="llama.jpg" data-caption="Loquacious llama!" data-align="left foobar" />'; $expected = '<figure class="caption caption-img"><img src="llama.jpg" /><figcaption>Loquacious llama!</figcaption></figure>'; - $this->assertIdentical($expected, $test($input)); + $output = $test($input); + $this->assertIdentical($expected, $output->getProcessedText()); + $this->assertIdentical($attached_library, $output->getAssets()); // Ensure the filter also works with uncommon yet valid attribute quoting. $input = '<img src=llama.jpg data-caption=\'Loquacious llama!\' data-align=right />'; $expected = '<figure class="caption caption-img align-right"><img src="llama.jpg" /><figcaption>Loquacious llama!</figcaption></figure>'; - $this->assertIdentical($expected, $test($input)); + $output = $test($input); + $this->assertIdentical($expected, $output->getProcessedText()); + $this->assertIdentical($attached_library, $output->getAssets()); // Security test: attempt to inject an additional class. $input = '<img src="llama.jpg" data-caption="Loquacious llama!" data-align="center another-class-here" />'; $expected = '<figure class="caption caption-img"><img src="llama.jpg" /><figcaption>Loquacious llama!</figcaption></figure>'; - $this->assertIdentical($expected, $test($input)); + $output = $test($input); + $this->assertIdentical($expected, $output->getProcessedText()); + $this->assertIdentical($attached_library, $output->getAssets()); // Security test: attempt an XSS. $input = '<img src="llama.jpg" data-caption="Loquacious llama!" data-align="center \'onclick=\'alert(foo);" />'; $expected = '<figure class="caption caption-img"><img src="llama.jpg" /><figcaption>Loquacious llama!</figcaption></figure>'; - $this->assertIdentical($expected, $test($input)); + $output = $test($input); + $this->assertIdentical($expected, $output->getProcessedText()); + $this->assertIdentical($attached_library, $output->getAssets()); // Finally, ensure that this also works on any other tag. $input = '<video src="llama.jpg" data-caption="Loquacious llama!" />'; $expected = '<figure class="caption caption-video"><video src="llama.jpg"></video><figcaption>Loquacious llama!</figcaption></figure>'; - $this->assertIdentical($expected, $test($input)); + $output = $test($input); + $this->assertIdentical($expected, $output->getProcessedText()); + $this->assertIdentical($attached_library, $output->getAssets()); $input = '<foobar data-caption="Loquacious llama!">baz</foobar>'; $expected = '<figure class="caption caption-foobar"><foobar>baz</foobar><figcaption>Loquacious llama!</figcaption></figure>'; - $this->assertIdentical($expected, $test($input)); + $output = $test($input); + $this->assertIdentical($expected, $output->getProcessedText()); + $this->assertIdentical($attached_library, $output->getAssets()); } /** @@ -676,7 +710,7 @@ www.example.com with a newline in comments --> */ function assertFilteredString($filter, $tests) { foreach ($tests as $source => $tasks) { - $result = $filter->process($source, $filter, FALSE, ''); + $result = $filter->process($source, $filter)->getProcessedText(); foreach ($tasks as $value => $is_expected) { // Not using assertIdentical, since combination with strpos() is hard to grok. if ($is_expected) { diff --git a/core/modules/filter/tests/filter_test/config/install/filter.format.filter_test.yml b/core/modules/filter/tests/filter_test/config/install/filter.format.filter_test.yml index 9b48b54..d3d77fc 100644 --- a/core/modules/filter/tests/filter_test/config/install/filter.format.filter_test.yml +++ b/core/modules/filter/tests/filter_test/config/install/filter.format.filter_test.yml @@ -4,7 +4,6 @@ weight: 2 roles: - anonymous - authenticated -cache: true status: true langcode: en filters: diff --git a/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestAssets.php b/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestAssets.php new file mode 100644 index 0000000..a7de6d9 --- /dev/null +++ b/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestAssets.php @@ -0,0 +1,38 @@ +<?php + +/** + * @file + * Contains \Drupal\filter_test\Plugin\Filter\FilterTestAssets. + */ + +namespace Drupal\filter_test\Plugin\Filter; + +use Drupal\filter\FilterProcessResult; +use Drupal\filter\Plugin\FilterBase; + +/** + * Provides a test filter to attach assets + * + * @Filter( + * id = "filter_test_assets", + * title = @Translation("Testing filter"), + * description = @Translation("Does not change content; attaches assets."), + * type = Drupal\filter\Plugin\FilterInterface::TYPE_TRANSFORM_REVERSIBLE + * ) + */ +class FilterTestAssets extends FilterBase { + + /** + * {@inheritdoc} + */ + public function process($text, $langcode) { + $result = new FilterProcessResult($text); + $result->addAssets(array( + 'library' => array( + 'filter/caption', + ), + )); + return $result; + } + +} diff --git a/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestCacheTags.php b/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestCacheTags.php new file mode 100644 index 0000000..3c03c3f --- /dev/null +++ b/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestCacheTags.php @@ -0,0 +1,35 @@ +<?php + +/** + * @file + * Contains \Drupal\filter_test\Plugin\Filter\FilterTestCacheTags. + */ + +namespace Drupal\filter_test\Plugin\Filter; + +use Drupal\filter\FilterProcessResult; +use Drupal\filter\Plugin\FilterBase; + +/** + * Provides a test filter to associate cache tags + * + * @Filter( + * id = "filter_test_cache_tags", + * title = @Translation("Testing filter"), + * description = @Translation("Does not change content; associates cache tags."), + * type = Drupal\filter\Plugin\FilterInterface::TYPE_TRANSFORM_REVERSIBLE + * ) + */ +class FilterTestCacheTags extends FilterBase { + + /** + * {@inheritdoc} + */ + public function process($text, $langcode) { + $result = new FilterProcessResult($text); + $result->addCacheTags(array('foo' => array('bar'))); + $result->addCacheTags(array('foo' => array('baz'))); + return $result; + } + +} diff --git a/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestPostRenderCache.php b/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestPostRenderCache.php new file mode 100644 index 0000000..aaeb400 --- /dev/null +++ b/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestPostRenderCache.php @@ -0,0 +1,59 @@ +<?php + +/** + * @file + * Contains \Drupal\filter_test\Plugin\Filter\FilterTestPostRenderCache. + */ + +namespace Drupal\filter_test\Plugin\Filter; + +use Drupal\filter\FilterProcessResult; +use Drupal\filter\Plugin\FilterBase; + +/** + * Provides a test filter to associate #post_render_cache callbacks. + * + * @Filter( + * id = "filter_test_post_render_cache", + * title = @Translation("Testing filter"), + * description = @Translation("Appends a placeholder to the content; associates #post_render_cache callbacks."), + * type = Drupal\filter\Plugin\FilterInterface::TYPE_TRANSFORM_REVERSIBLE + * ) + */ +class FilterTestPostRenderCache extends FilterBase { + + /** + * {@inheritdoc} + */ + public function process($text, $langcode) { + $callback = '\Drupal\filter_test\Plugin\Filter\FilterTestPostRenderCache::renderDynamicThing'; + $context = array( + 'thing' => 'llama', + ); + $placeholder = drupal_render_cache_generate_placeholder($callback, $context); + $result = new FilterProcessResult($text . '<p>' . $placeholder . '</p>'); + $result->addPostRenderCacheCallback($callback, $context); + return $result; + } + + /** + * #post_render_cache callback; replaces placeholder with a dynamic thing. + * + * @param array $element + * The renderable array that contains the to be replaced placeholder. + * @param array $context + * An array with the following keys: + * - thing: a "thing" string + * + * @return array + * A renderable array containing the comment form. + */ + public static function renderDynamicThing(array $element, array $context) { + $callback = '\Drupal\filter_test\Plugin\Filter\FilterTestPostRenderCache::renderDynamicThing'; + $placeholder = drupal_render_cache_generate_placeholder($callback, $context); + $markup = format_string('This is a dynamic @thing.', array('@thing' => $context['thing'])); + $element['#markup'] = str_replace($placeholder, $markup, $element['#markup']); + return $element; + } + +} diff --git a/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestReplace.php b/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestReplace.php index f48c5b1..735c02d 100644 --- a/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestReplace.php +++ b/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestReplace.php @@ -7,6 +7,7 @@ namespace Drupal\filter_test\Plugin\Filter; +use Drupal\filter\FilterProcessResult; use Drupal\filter\Plugin\FilterBase; /** @@ -24,15 +25,11 @@ class FilterTestReplace extends FilterBase { /** * {@inheritdoc} */ - public function process($text, $langcode, $cache, $cache_id) { + public function process($text, $langcode) { $text = array(); $text[] = 'Filter: ' . $this->getLabel() . ' (' . $this->getPluginId() . ')'; $text[] = 'Language: ' . $langcode; - $text[] = 'Cache: ' . ($cache ? 'Enabled' : 'Disabled'); - if ($cache_id) { - $text[] = 'Cache ID: ' . $cache_id; - } - return implode("<br />\n", $text); + return new FilterProcessResult(implode("<br />\n", $text)); } } diff --git a/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestRestrictTagsAndAttributes.php b/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestRestrictTagsAndAttributes.php index 6e95d2e..c11160d 100644 --- a/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestRestrictTagsAndAttributes.php +++ b/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestRestrictTagsAndAttributes.php @@ -7,6 +7,7 @@ namespace Drupal\filter_test\Plugin\Filter; +use Drupal\filter\FilterProcessResult; use Drupal\filter\Plugin\FilterBase; use Drupal\Component\Utility\Xss; @@ -25,11 +26,11 @@ class FilterTestRestrictTagsAndAttributes extends FilterBase { /** * {@inheritdoc} */ - public function process($text, $langcode, $cache, $cache_id) { + public function process($text, $langcode) { $allowed_tags = array_filter($this->settings['restrictions']['allowed'], function($value) { return is_array($value) || (bool) $value !== FALSE; }); - return Xss::filter($text, array_keys($allowed_tags)); + return new FilterProcessResult(Xss::filter($text, array_keys($allowed_tags))); } /** diff --git a/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestUncacheable.php b/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestUncacheable.php deleted file mode 100644 index fd8318f..0000000 --- a/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestUncacheable.php +++ /dev/null @@ -1,32 +0,0 @@ -<?php - -/** - * @file - * Contains \Drupal\filter_test\Plugin\Filter\FilterTestUncacheable. - */ - -namespace Drupal\filter_test\Plugin\Filter; - -use Drupal\filter\Plugin\FilterBase; - -/** - * Provides a test filter that is uncacheable. - * - * @Filter( - * id = "filter_test_uncacheable", - * title = @Translation("Uncacheable filter"), - * description = @Translation("Does nothing, but makes a text format uncacheable"), - * type = Drupal\filter\Plugin\FilterInterface::TYPE_TRANSFORM_IRREVERSIBLE, - * cache = false - * ) - */ -class FilterTestUncacheable extends FilterBase { - - /** - * {@inheritdoc} - */ - public function process($text, $langcode, $cache, $cache_id) { - return $text; - } - -} diff --git a/core/modules/system/src/Tests/Common/RenderTest.php b/core/modules/system/src/Tests/Common/RenderTest.php index 8edbb07..7587af6 100644 --- a/core/modules/system/src/Tests/Common/RenderTest.php +++ b/core/modules/system/src/Tests/Common/RenderTest.php @@ -774,6 +774,8 @@ class RenderTest extends DrupalUnitTestBase { ); $callback = 'common_test_post_render_cache_placeholder'; $placeholder = drupal_render_cache_generate_placeholder($callback, $context); + $this->assertIdentical($placeholder, Html::normalize($placeholder), 'Placeholder unaltered by Html::normalize() which is used by FilterHtmlCorrector.'); + $test_element = array( '#post_render_cache' => array( $callback => array( @@ -825,7 +827,7 @@ class RenderTest extends DrupalUnitTestBase { $this->assertIdentical($token, $expected_token, 'The tokens are identical'); // Verify the token is in the cached element. $expected_element = array( - '#markup' => '<foo><drupal:render-cache-placeholder callback="common_test_post_render_cache_placeholder" token="'. $expected_token . '" /></foo>', + '#markup' => '<foo><drupal-render-cache-placeholder callback="common_test_post_render_cache_placeholder" token="'. $expected_token . '"></drupal-render-cache-placeholder></foo>', '#post_render_cache' => array( 'common_test_post_render_cache_placeholder' => array( $context @@ -923,7 +925,7 @@ class RenderTest extends DrupalUnitTestBase { $this->assertIdentical($token, $expected_token, 'The tokens are identical for the child element'); // Verify the token is in the cached element. $expected_element = array( - '#markup' => '<foo><drupal:render-cache-placeholder callback="common_test_post_render_cache_placeholder" token="'. $expected_token . '" /></foo>', + '#markup' => '<foo><drupal-render-cache-placeholder callback="common_test_post_render_cache_placeholder" token="'. $expected_token . '"></drupal-render-cache-placeholder></foo>', '#post_render_cache' => array( 'common_test_post_render_cache_placeholder' => array( $context, @@ -948,7 +950,7 @@ class RenderTest extends DrupalUnitTestBase { $this->assertIdentical($token, $expected_token, 'The tokens are identical for the parent element'); // Verify the token is in the cached element. $expected_element = array( - '#markup' => '<div><foo><drupal:render-cache-placeholder callback="common_test_post_render_cache_placeholder" token="'. $expected_token . '" /></foo></div>' . "\n", + '#markup' => '<div><foo><drupal-render-cache-placeholder callback="common_test_post_render_cache_placeholder" token="'. $expected_token . '"></drupal-render-cache-placeholder></foo></div>' . "\n", '#post_render_cache' => array( 'common_test_post_render_cache_placeholder' => array( $context, @@ -977,7 +979,7 @@ class RenderTest extends DrupalUnitTestBase { $this->assertIdentical($token, $expected_token, 'The tokens are identical for the child element'); // Verify the token is in the cached element. $expected_element = array( - '#markup' => '<foo><drupal:render-cache-placeholder callback="common_test_post_render_cache_placeholder" token="'. $expected_token . '" /></foo>', + '#markup' => '<foo><drupal-render-cache-placeholder callback="common_test_post_render_cache_placeholder" token="'. $expected_token . '"></drupal-render-cache-placeholder></foo>', '#post_render_cache' => array( 'common_test_post_render_cache_placeholder' => array( $context, diff --git a/core/modules/text/src/Plugin/Field/FieldFormatter/TextDefaultFormatter.php b/core/modules/text/src/Plugin/Field/FieldFormatter/TextDefaultFormatter.php index 5e0f80b..cfd83dc 100644 --- a/core/modules/text/src/Plugin/Field/FieldFormatter/TextDefaultFormatter.php +++ b/core/modules/text/src/Plugin/Field/FieldFormatter/TextDefaultFormatter.php @@ -32,18 +32,67 @@ class TextDefaultFormatter extends FormatterBase { * {@inheritdoc} */ public function viewElements(FieldItemListInterface $items) { + if ($this->getFieldSetting('text_processing')) { + return $this->viewElementsWithTextProcessing($items); + } + else { + return $this->viewElementsWithoutTextProcessing($items); + } + } + + /** + * Builds a renderable array when text processing is enabled. + * + * @param \Drupal\Core\Field\FieldItemListInterface $items + * The text field values to be rendered. + * + * @return array + * A renderable array for $items, as an array of child elements keyed by + * consecutive numeric indexes starting from 0. + */ + protected function viewElementsWithTextProcessing(FieldItemListInterface $items) { + $elements = array(); + + foreach ($items as $delta => $item) { + $elements[$delta] = array( + '#type' => 'processed_text', + '#text' => $item->value, + '#format' => $item->format, + '#langcode' => $item->getLangcode(), + ); + // The viewElements() method of entity field formatters is run + // during the #pre_render phase of rendering an entity. A formatter + // builds the content of the field in preparation for theming. + // All cache tags must be available after the #pre_render phase. In order + // to collect the cache tags associated with the processed text, it must + // be passed to drupal_render() so that its #pre_render callback is + // invoked and its full build array is assembled. Rendering the processed + // text in place here will allow its cache tags to be bubbled up and + // included with those of the main entity when cache tags are collected + // for a renderable array in drupal_render(). + // @todo remove this work-around, see https://drupal.org/node/2273277 + drupal_render($elements[$delta], TRUE); + } + + return $elements; + } + + /** + * Builds a renderable array when text processing is disabled. + * + * @param \Drupal\Core\Field\FieldItemListInterface $items + * The text field values to be rendered. + * + * @return array + * A renderable array for $items, as an array of child elements keyed by + * consecutive numeric indexes starting from 0. + */ + protected function viewElementsWithoutTextProcessing(FieldItemListInterface $items) { $elements = array(); foreach ($items as $delta => $item) { $elements[$delta] = array( - '#markup' => $item->processed, - '#cache' => array( - 'tags' => array( - 'filter_format' => array( - $item->format, - ), - ), - ), + '#markup' => $item->processed, ); } diff --git a/core/modules/text/src/Plugin/Field/FieldFormatter/TextTrimmedFormatter.php b/core/modules/text/src/Plugin/Field/FieldFormatter/TextTrimmedFormatter.php index d4b3c34..f2bbefc 100644 --- a/core/modules/text/src/Plugin/Field/FieldFormatter/TextTrimmedFormatter.php +++ b/core/modules/text/src/Plugin/Field/FieldFormatter/TextTrimmedFormatter.php @@ -68,26 +68,84 @@ class TextTrimmedFormatter extends FormatterBase { * {@inheritdoc} */ public function viewElements(FieldItemListInterface $items) { + if ($this->getFieldSetting('text_processing')) { + return $this->viewElementsWithTextProcessing($items); + } + else { + return $this->viewElementsWithoutTextProcessing($items); + } + } + + /** + * Builds a renderable array when text processing is enabled. + * + * @param \Drupal\Core\Field\FieldItemListInterface $items + * The text field values to be rendered. + * + * @return array + * A renderable array for $items, as an array of child elements keyed by + * consecutive numeric indexes starting from 0. + */ + protected function viewElementsWithTextProcessing(FieldItemListInterface $items) { + $elements = array(); + + foreach ($items as $delta => $item) { + $elements[$delta] = array( + '#type' => 'processed_text', + '#text' => NULL, + '#format' => $item->format, + '#langcode' => $item->getLangcode(), + ); + + // The viewElements() method of entity field formatters is run + // during the #pre_render phase of rendering an entity. A formatter + // builds the content of the field in preparation for theming. + // All cache tags must be available after the #pre_render phase. In order + // to collect the cache tags associated with the processed text, it must + // be passed to drupal_render() so that its #pre_render callback is + // invoked and its full build array is assembled. Rendering the processed + // text in place here will allow its cache tags to be bubbled up and + // included with those of the main entity when cache tags are collected + // for a renderable array in drupal_render(). + if ($this->getPluginId() == 'text_summary_or_trimmed' && !empty($item->summary)) { + $elements[$delta]['#text'] = $item->summary; + // @todo remove this work-around, see https://drupal.org/node/2273277 + drupal_render($elements[$delta], TRUE); + } + else { + $elements[$delta]['#text'] = $item->value; + // @todo remove this work-around, see https://drupal.org/node/2273277 + drupal_render($elements[$delta], TRUE); + $elements[$delta]['#markup'] = text_summary($elements[$delta]['#markup'], $item->format, $this->getSetting('trim_length')); + } + } + + return $elements; + } + + /** + * Builds a renderable array when text processing is disabled. + * + * @param \Drupal\Core\Field\FieldItemListInterface $items + * The text field values to be rendered. + * + * @return array + * A renderable array for $items, as an array of child elements keyed by + * consecutive numeric indexes starting from 0. + */ + protected function viewElementsWithoutTextProcessing(FieldItemListInterface $items) { $elements = array(); - $text_processing = $this->getFieldSetting('text_processing'); foreach ($items as $delta => $item) { if ($this->getPluginId() == 'text_summary_or_trimmed' && !empty($item->summary)) { $output = $item->summary_processed; } else { - $output = $item->processed; - $output = text_summary($output, $text_processing ? $item->format : NULL, $this->getSetting('trim_length')); + $output = text_summary($item->processed, NULL, $this->getSetting('trim_length')); } + $elements[$delta] = array( '#markup' => $output, - '#cache' => array( - 'tags' => array( - 'filter_format' => array( - $item->format, - ), - ), - ), ); } diff --git a/core/modules/text/src/Tests/Formatter/TextFormatterTest.php b/core/modules/text/src/Tests/Formatter/TextFormatterTest.php new file mode 100644 index 0000000..3745a5a --- /dev/null +++ b/core/modules/text/src/Tests/Formatter/TextFormatterTest.php @@ -0,0 +1,137 @@ +<?php + +/** + * @file + * Contains \Drupal\text\Tests\Formatter\TextFormatterTest. + */ + +namespace Drupal\text\Tests\Formatter; + +use Drupal\system\Tests\Entity\EntityUnitTestBase; + +/** + * Tests Text formatters. + */ +class TextFormatterTest extends EntityUnitTestBase { + + /** + * The entity type used in this test. + * + * @var string + */ + protected $entityType = 'entity_test'; + + /** + * The bundle used in this test. + * + * @var string + */ + protected $bundle = 'entity_test'; + + /** + * Modules to enable. + * + * @var array + */ + public static $modules = array('text'); + + /** + * {@inheritdoc} + */ + public static function getInfo() { + return array( + 'name' => 'Text formatters', + 'description' => 'Tests the text formatters functionality.', + 'group' => 'Text ', + ); + } + + /** + * {@inheritdoc} + */ + public function setUp() { + parent::setUp(); + + entity_create('filter_format', array( + 'format' => 'my_text_format', + 'name' => 'My text format', + 'filters' => array( + 'filter_autop' => array( + 'module' => 'filter', + 'status' => TRUE, + ), + ), + ))->save(); + + // Set up two fields: one with text processing enabled, the other disabled. + entity_create('field_config', array( + 'name' => 'processed_text', + 'entity_type' => $this->entityType, + 'type' => 'text', + 'settings' => array(), + ))->save(); + entity_create('field_instance_config', array( + 'entity_type' => $this->entityType, + 'bundle' => $this->bundle, + 'field_name' => 'processed_text', + 'label' => 'Processed text', + 'settings' => array( + 'text_processing' => TRUE, + ), + ))->save(); + entity_create('field_config', array( + 'name' => 'unprocessed_text', + 'entity_type' => $this->entityType, + 'type' => 'text', + 'settings' => array(), + ))->save(); + entity_create('field_instance_config', array( + 'entity_type' => $this->entityType, + 'bundle' => $this->bundle, + 'field_name' => 'unprocessed_text', + 'label' => 'Unprocessed text', + 'settings' => array( + 'text_processing' => FALSE, + ), + ))->save(); + } + + /** + * Tests all text field formatters. + */ + public function testFormatters() { + $formatters = array( + 'text_default', + 'text_trimmed', + 'text_summary_or_trimmed', + ); + + // Create the entity to be referenced. + $entity = entity_create($this->entityType, array('name' => $this->randomName())); + $entity->processed_text = array( + 'value' => 'Hello, world!', + 'format' => 'my_text_format', + ); + $entity->unprocessed_text = array( + 'value' => 'Hello, world!', + ); + $entity->save(); + + foreach ($formatters as $formatter) { + // Verify the processed text field formatter's render array. + $build = $entity->get('processed_text')->view(array('type' => $formatter)); + $this->assertEqual($build[0]['#markup'], "<p>Hello, world!</p>\n"); + $expected_cache_tags = array( + 'filter_format' => array('my_text_format' => 'my_text_format'), + ); + $this->assertEqual($build[0]['#cache']['tags'], $expected_cache_tags, format_string('The @formatter formatter has the expected cache tags when formatting a processed text field.', array('@formatter' => $formatter))); + + // Verify the unprocessed text field formatter's render array. + $build = $entity->get('unprocessed_text')->view(array('type' => $formatter)); + debug($build[0]); + $this->assertEqual($build[0]['#markup'], 'Hello, world!'); + $this->assertTrue(!isset($build[0]['#cache']), format_string('The @formatter formatter has the expected cache tags when formatting an unprocessed text field.', array('@formatter' => $formatter))); + } + } + +} diff --git a/core/modules/text/src/TextProcessed.php b/core/modules/text/src/TextProcessed.php index 5d99de3..3666d29 100644 --- a/core/modules/text/src/TextProcessed.php +++ b/core/modules/text/src/TextProcessed.php @@ -34,7 +34,7 @@ class TextProcessed extends TypedData { parent::__construct($definition, $name, $parent); if ($definition->getSetting('text source') === NULL) { - throw new \InvalidArgumentException("The definition's 'source' key has to specify the name of the text property to be processed."); + throw new \InvalidArgumentException("The definition's 'text source' key has to specify the name of the text property to be processed."); } } diff --git a/core/modules/user/src/Tests/UserSignatureTest.php b/core/modules/user/src/Tests/UserSignatureTest.php index 83f5973..ce6c5ff 100644 --- a/core/modules/user/src/Tests/UserSignatureTest.php +++ b/core/modules/user/src/Tests/UserSignatureTest.php @@ -68,7 +68,7 @@ class UserSignatureTest extends WebTestBase { // Create regular and administrative users. $this->web_user = $this->drupalCreateUser(array('post comments')); - $admin_permissions = array('administer comments'); + $admin_permissions = array('post comments', 'administer comments'); foreach (filter_formats() as $format) { if ($permission = $format->getPermissionName()) { $admin_permissions[] = $permission; @@ -82,7 +82,14 @@ class UserSignatureTest extends WebTestBase { * upon display. */ function testUserSignature() { - $node = $this->drupalCreateNode(); + $node = $this->drupalCreateNode(array( + 'body' => array( + 0 => array( + 'value' => $this->randomName(32), + 'format' => 'full_html', + ), + ), + )); // Verify that user signature field is not displayed on registration form. $this->drupalGet('user/register'); @@ -99,6 +106,10 @@ class UserSignatureTest extends WebTestBase { // Verify that values were stored. $this->assertFieldByName('signature[value]', $edit['signature[value]'], 'Submitted signature text found.'); + // Verify that the user signature's text format's cache tag is absent. + $this->drupalGet('node/' . $node->id()); + $this->assertTrue(!in_array('filter_format:filtered_html_format', explode(' ', $this->drupalGetHeader('X-Drupal-Cache-Tags')))); + // Create a comment. $edit = array(); $edit['subject'] = $this->randomName(8); @@ -121,5 +132,8 @@ class UserSignatureTest extends WebTestBase { $this->drupalGet('node/' . $node->id()); $this->assertNoRaw($signature_text, 'Unfiltered signature text not found.'); $this->assertRaw(check_markup($signature_text, $this->filtered_html_format->format), 'Filtered signature text found.'); + // Verify that the user signature's text format's cache tag is present. + $this->drupalGet('node/' . $node->id()); + $this->assertTrue(in_array('filter_format:filtered_html_format', explode(' ', $this->drupalGetHeader('X-Drupal-Cache-Tags')))); } } diff --git a/core/modules/views/src/Plugin/views/area/Text.php b/core/modules/views/src/Plugin/views/area/Text.php index 86288b0..de89210 100644 --- a/core/modules/views/src/Plugin/views/area/Text.php +++ b/core/modules/views/src/Plugin/views/area/Text.php @@ -58,20 +58,13 @@ class Text extends TokenizeAreaPluginBase { $format = isset($this->options['format']) ? $this->options['format'] : filter_default_format(); if (!$empty || !empty($this->options['empty'])) { return array( - '#markup' => $this->renderTextarea($this->options['content'], $format), + '#type' => 'processed_text', + '#text' => $this->tokenizeValue($this->options['content']), + '#format' => $format, ); } return array(); } - /** - * Render a text area, using the proper format. - */ - public function renderTextarea($value, $format) { - if ($value) { - return check_markup($this->tokenizeValue($value), $format, '', FALSE); - } - } - } diff --git a/core/modules/views/src/Plugin/views/field/Markup.php b/core/modules/views/src/Plugin/views/field/Markup.php index ff43934..465f655 100644 --- a/core/modules/views/src/Plugin/views/field/Markup.php +++ b/core/modules/views/src/Plugin/views/field/Markup.php @@ -53,7 +53,7 @@ class Markup extends FieldPluginBase { } if ($value) { $value = str_replace('<!--break-->', '', $value); - return check_markup($value, $format, ''); + return check_markup($value, $format); } } diff --git a/core/modules/views/src/Tests/Handler/AreaTextTest.php b/core/modules/views/src/Tests/Handler/AreaTextTest.php index 3ab29d7..7bc2bc2 100644 --- a/core/modules/views/src/Tests/Handler/AreaTextTest.php +++ b/core/modules/views/src/Tests/Handler/AreaTextTest.php @@ -60,16 +60,19 @@ class AreaTextTest extends ViewUnitTestBase { $this->executeView($view); $view->display_handler->handlers['header']['area']->options['format'] = $this->randomString(); - $this->assertEqual(array('#markup' => ''), $view->display_handler->handlers['header']['area']->render(), 'Nonexistent format should return empty markup.'); + $build = $view->display_handler->handlers['header']['area']->render(); + $this->assertEqual('', drupal_render($build), 'Nonexistent format should return empty markup.'); $view->display_handler->handlers['header']['area']->options['format'] = filter_default_format(); - $this->assertEqual(array('#markup' => check_markup($string)), $view->display_handler->handlers['header']['area']->render(), 'Existant format should return something'); + $build = $view->display_handler->handlers['header']['area']->render(); + $this->assertEqual(check_markup($string), drupal_render($build), 'Existent format should return something'); // Empty results, and it shouldn't be displayed . $this->assertEqual(array(), $view->display_handler->handlers['header']['area']->render(TRUE), 'No result should lead to no header'); // Empty results, and it should be displayed. $view->display_handler->handlers['header']['area']->options['empty'] = TRUE; - $this->assertEqual(array('#markup' => check_markup($string)), $view->display_handler->handlers['header']['area']->render(TRUE), 'No result, but empty enabled lead to a full header'); + $build = $view->display_handler->handlers['header']['area']->render(TRUE); + $this->assertEqual(check_markup($string), drupal_render($build), 'No result, but empty enabled lead to a full header'); } } diff --git a/core/profiles/standard/config/install/filter.format.basic_html.yml b/core/profiles/standard/config/install/filter.format.basic_html.yml index 0a9ad13..810a484 100644 --- a/core/profiles/standard/config/install/filter.format.basic_html.yml +++ b/core/profiles/standard/config/install/filter.format.basic_html.yml @@ -4,7 +4,6 @@ status: true weight: 0 roles: - authenticated -cache: true filters: filter_html: id: filter_html @@ -33,4 +32,13 @@ filters: status: true weight: 10 settings: { } + editor_file_reference: + id: editor_file_reference + provider: editor + status: true + weight: 11 + settings: { } langcode: en +dependencies: + module: + - editor diff --git a/core/profiles/standard/config/install/filter.format.full_html.yml b/core/profiles/standard/config/install/filter.format.full_html.yml index 31b6692..9f5ea79 100644 --- a/core/profiles/standard/config/install/filter.format.full_html.yml +++ b/core/profiles/standard/config/install/filter.format.full_html.yml @@ -4,7 +4,6 @@ status: true weight: 1 roles: - administrator -cache: true filters: filter_caption: id: filter_caption @@ -18,4 +17,13 @@ filters: status: true weight: 10 settings: { } + editor_file_reference: + id: editor_file_reference + provider: editor + status: true + weight: 11 + settings: { } langcode: en +dependencies: + module: + - editor diff --git a/core/profiles/standard/config/install/filter.format.restricted_html.yml b/core/profiles/standard/config/install/filter.format.restricted_html.yml index 12a94aa..95a20e5 100644 --- a/core/profiles/standard/config/install/filter.format.restricted_html.yml +++ b/core/profiles/standard/config/install/filter.format.restricted_html.yml @@ -4,7 +4,6 @@ status: true weight: 0 roles: - anonymous -cache: true filters: filter_html: id: filter_html |