Commit d5be5c04 authored by alexpott's avatar alexpott
Browse files

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
......
......@@ -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,9 +99,131 @@ 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']);