Commit d5be5c04 authored by alexpott's avatar alexpott

Issue #2217877 by Wim Leers, Dave Reid, damiankloip, xjm, chx | catch: Fixed...

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.
parent e05d622e
......@@ -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>';
}
/**
......
......@@ -9,8 +9,12 @@
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 @@
* 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 @@ public function getButtons() {
* 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;
}
/**
......
......@@ -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'] = '';
......
......@@ -131,6 +131,21 @@ public function buildComponents(array &$build, array $entities, array $displays,
'#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();
}
......
......@@ -47,7 +47,7 @@ public function getUntransformedText(EntityInterface $entity, $field_name, $lang
// 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;
......
<?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;
}
}
<?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());
}
}
......@@ -106,6 +106,7 @@ public function viewElements(FieldItemListInterface $items) {
// 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;
......
......@@ -60,8 +60,6 @@ public static function getInfo() {
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 @@ public function setUp() {
))
->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 @@ public function testEntityFormatter() {
$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:&nbsp;</div>
<div class="field-items">
<div class="field-item"></div>
<div class="field-item"><p>Hello, world!</p></div>
</div>
</div>
';
......
......@@ -9,7 +9,6 @@ weight: 10
roles:
- anonymous
- authenticated
cache: true
filters:
# Escape all HTML.
filter_html_escape:
......
......@@ -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
......
This diff is collapsed.
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
......@@ -64,16 +64,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.
*
......
......@@ -92,13 +92,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.
*
......@@ -207,15 +200,6 @@ public function preSave(EntityStorageInterface $storage) {
// @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;
}
}
}
/**
......
<?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.
* <