Commit 7d1c02a7 authored by catch's avatar catch

Issue #2247779 by Wim Leers, larowlan, dawehner: Allow #pre_render,...

Issue #2247779 by Wim Leers, larowlan, dawehner: Allow #pre_render, #post_render and #post_render_cache callbacks to be service methods.
parent b1b3b2b8
......@@ -3305,8 +3305,16 @@ function drupal_render(&$elements, $is_recursive_call = FALSE) {
// Make any final changes to the element before it is rendered. This means
// that the $element or the children can be altered or corrected before the
// element is rendered into the final text.
/** @var \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver */
$controller_resolver = \Drupal::service('controller_resolver');
if (isset($elements['#pre_render'])) {
foreach ($elements['#pre_render'] as $callable) {
if (is_string($callable) && strpos($callable, '::') === FALSE) {
$callable = $controller_resolver->getControllerFromDefinition($callable);
}
else {
$callable = $callable;
}
$elements = call_user_func($callable, $elements);
}
}
......@@ -3407,6 +3415,12 @@ function drupal_render(&$elements, $is_recursive_call = FALSE) {
// which allows the output'ed text to be filtered.
if (isset($elements['#post_render'])) {
foreach ($elements['#post_render'] as $callable) {
if (is_string($callable) && strpos($callable, '::') === FALSE) {
$callable = $controller_resolver->getControllerFromDefinition($callable);
}
else {
$callable = $callable;
}
$elements['#children'] = call_user_func($callable, $elements['#children'], $elements);
}
}
......@@ -3672,9 +3686,24 @@ function drupal_render_cache_set(&$markup, array $elements) {
* @return string
* The generated placeholder HTML.
*
* @throws \Exception
*
* @see drupal_render_cache_get()
*/
function drupal_render_cache_generate_placeholder($callback, array &$context) {
if (is_string($callback) && strpos($callback, '::') === FALSE) {
/** @var \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver */
$controller_resolver = \Drupal::service('controller_resolver');
$callable = \Drupal::service('controller_resolver')->getControllerFromDefinition($callback);
}
else {
$callable = $callback;
}
if (!is_callable($callable)) {
throw new Exception(t('$callable must be a callable function or of the form service_id:method.'));
}
// Generate a unique token if one is not already provided.
$context += array(
'token' => \Drupal\Component\Utility\Crypt::randomBytesBase64(55),
......@@ -3702,10 +3731,19 @@ function drupal_render_cache_generate_placeholder($callback, array &$context) {
*/
function _drupal_render_process_post_render_cache(array &$elements) {
if (isset($elements['#post_render_cache'])) {
/** @var \Drupal\Core\Controller\ControllerResolverInterface $controller_resolver */
$controller_resolver = \Drupal::service('controller_resolver');
// Call all #post_render_cache callbacks, passing the provided context.
foreach (array_keys($elements['#post_render_cache']) as $callback) {
if (strpos($callback, '::') === FALSE) {
$callable = $controller_resolver->getControllerFromDefinition($callback);
}
else {
$callable = $callback;
}
foreach ($elements['#post_render_cache'][$callback] as $context) {
$elements = call_user_func_array($callback, array($elements, $context));
$elements = call_user_func_array($callable, array($elements, $context));
}
}
// Make sure that any attachments added in #post_render_cache callbacks are
......
......@@ -12,3 +12,7 @@ services:
comment.statistics:
class: Drupal\comment\CommentStatistics
arguments: ['@database', '@current_user', '@entity.manager', '@state']
comment.post_render_cache:
class: Drupal\comment\CommentPostRenderCache
arguments: ['@entity.manager', '@entity.form_builder']
<?php
/**
* @file
* Contains \Drupal\comment\CommentPostRenderCache.
*/
namespace Drupal\comment;
use Drupal\Core\Entity\EntityFormBuilderInterface;
use Drupal\Core\Entity\EntityManagerInterface;
use Drupal\field\Entity\FieldConfig;
/**
* Defines a service for comment post render cache callbacks.
*/
class CommentPostRenderCache {
/**
* The entity manager service.
*
* @var \Drupal\Core\Entity\EntityManagerInterface
*/
protected $entityManager;
/**
* The entity form builder service.
*
* @var \Drupal\Core\Entity\EntityFormBuilderInterface
*/
protected $entityFormBuilder;
/**
* Constructs a new CommentPostRenderCache object.
*
* @param \Drupal\Core\Entity\EntityManagerInterface $entity_manager
* The entity manager service.
* @param \Drupal\Core\Entity\EntityFormBuilderInterface $entity_form_builder
* The entity form builder service.
*/
public function __construct(EntityManagerInterface $entity_manager, EntityFormBuilderInterface $entity_form_builder) {
$this->entityManager = $entity_manager;
$this->entityFormBuilder = $entity_form_builder;
}
/**
* #post_render_cache callback; replaces placeholder with comment form.
*
* @param array $element
* The renderable array that contains the to be replaced placeholder.
* @param array $context
* An array with the following keys:
* - entity_type: an entity type
* - entity_id: an entity ID
* - field_name: a comment field name
*
* @return array
* A renderable array containing the comment form.
*/
public function renderForm(array $element, array $context) {
$field_name = $context['field_name'];
$entity = $this->entityManager->getStorage($context['entity_type'])->load($context['entity_id']);
$field = Fieldconfig::loadByName($entity->getEntityTypeId(), $field_name);
$values = array(
'entity_type' => $entity->getEntityTypeId(),
'entity_id' => $entity->id(),
'field_name' => $field_name,
'comment_type' => $field->getSetting('bundle'),
'pid' => NULL,
);
$comment = $this->entityManager->getStorage('comment')->create($values);
$form = $this->entityFormBuilder->getForm($comment);
// @todo: This only works as long as assets are still tracked in a global
// static variable, see https://drupal.org/node/2238835
$markup = drupal_render($form, TRUE);
$callback = 'comment.post_render_cache:renderForm';
$placeholder = drupal_render_cache_generate_placeholder($callback, $context);
$element['#markup'] = str_replace($placeholder, $markup, $element['#markup']);
return $element;
}
}
......@@ -176,7 +176,7 @@ public function viewElements(FieldItemListInterface $items) {
// All other users need a user-specific form, which would break the
// render cache: hence use a #post_render_cache callback.
else {
$callback = '\Drupal\comment\Plugin\Field\FieldFormatter\CommentDefaultFormatter::renderForm';
$callback = 'comment.post_render_cache:renderForm';
$context = array(
'entity_type' => $entity->getEntityTypeId(),
'entity_id' => $entity->id(),
......@@ -207,33 +207,6 @@ public function viewElements(FieldItemListInterface $items) {
return $elements;
}
/**
* #post_render_cache callback; replaces placeholder with comment form.
*
* @param array $element
* The renderable array that contains the to be replaced placeholder.
* @param array $context
* An array with the following keys:
* - entity_type: an entity type
* - entity_id: an entity ID
* - field_name: a comment field name
*
* @return array
* A renderable array containing the comment form.
*/
public static function renderForm(array $element, array $context) {
$callback = '\Drupal\comment\Plugin\Field\FieldFormatter\CommentDefaultFormatter::renderForm';
$placeholder = drupal_render_cache_generate_placeholder($callback, $context);
$entity = entity_load($context['entity_type'], $context['entity_id']);
$form = comment_add($entity, $context['field_name']);
// @todo: This only works as long as assets are still tracked in a global
// static variable, see https://drupal.org/node/2238835
$markup = drupal_render($form, TRUE);
$element['#markup'] = str_replace($placeholder, $markup, $element['#markup']);
return $element;
}
/**
* {@inheritdoc}
*/
......
......@@ -61,7 +61,7 @@ function editor_menu_link_defaults_alter(array &$links) {
*/
function editor_element_info() {
$type['text_format'] = array(
'#pre_render' => array('editor_pre_render_format'),
'#pre_render' => array('element.editor:preRenderTextFormat'),
);
return $type;
}
......@@ -246,88 +246,6 @@ function editor_load($format_id) {
return isset($editors[$format_id]) ? $editors[$format_id] : NULL;
}
/**
* Additional #pre_render callback for 'text_format' elements.
*/
function editor_pre_render_format($element) {
// Allow modules to programmatically enforce no client-side editor by setting
// the #editor property to FALSE.
if (isset($element['#editor']) && !$element['#editor']) {
return $element;
}
// filter_process_format() copies properties to the expanded 'value' child
// element, including the #pre_render property. Skip this text format widget,
// if it contains no 'format'.
if (!isset($element['format'])) {
return $element;
}
$format_ids = array_keys($element['format']['format']['#options']);
// Early-return if no text editor is associated with any of the text formats.
$editors = entity_load_multiple('editor', $format_ids);
if (count($editors) === 0) {
return $element;
}
// Use a hidden element for a single text format.
$field_id = $element['value']['#id'];
if (!$element['format']['format']['#access']) {
// Use the first (and only) available text format.
$format_id = $format_ids[0];
$element['format']['editor'] = array(
'#type' => 'hidden',
'#name' => $element['format']['format']['#name'],
'#value' => $format_id,
'#attributes' => array(
'class' => array('editor'),
'data-editor-for' => $field_id,
),
);
}
// Otherwise, attach to text format selector.
else {
$element['format']['format']['#attributes']['class'][] = 'editor';
$element['format']['format']['#attributes']['data-editor-for'] = $field_id;
}
// Hide the text format's filters' guidelines of those text formats that have
// a text editor associated: they're rather useless when using a text editor.
foreach ($editors as $format_id => $editor) {
$element['format']['guidelines'][$format_id]['#access'] = FALSE;
}
// Attach Text Editor module's (this module) library.
$element['#attached']['library'][] = 'editor/drupal.editor';
// Attach attachments for all available editors.
$manager = \Drupal::service('plugin.manager.editor');
$element['#attached'] = NestedArray::mergeDeep($element['#attached'], $manager->getAttachments($format_ids));
// Apply XSS filters when editing content if necessary. Some types of text
// editors cannot guarantee that the end user won't become a victim of XSS.
if (!empty($element['value']['#value'])) {
$original = $element['value']['#value'];
$format = entity_load('filter_format', $element['format']['format']['#value']);
// Ensure XSS-safety for the current text format/editor.
$filtered = editor_filter_xss($original, $format);
if ($filtered !== FALSE) {
$element['value']['#value'] = $filtered;
}
// Only when the user has access to multiple text formats, we must add data-
// attributes for the original value and change tracking, because they are
// only necessary when the end user can switch between text formats/editors.
if ($element['format']['format']['#access']) {
$element['value']['#attributes']['data-editor-value-is-changed'] = 'false';
$element['value']['#attributes']['data-editor-value-original'] = $original;
}
}
return $element;
}
/**
* Applies text editor XSS filtering.
*
......
......@@ -2,3 +2,6 @@ services:
plugin.manager.editor:
class: Drupal\editor\Plugin\EditorManager
parent: default_plugin_manager
element.editor:
class: Drupal\editor\Element
arguments: ['@plugin.manager.editor']
<?php
/**
* @file
* Contains \Drupal\editor\Element.
*/
namespace Drupal\editor;
use Drupal\editor\Entity\Editor;
use Drupal\filter\Entity\FilterFormat;
use Drupal\Component\Plugin\PluginManagerInterface;
/**
* Defines a service for Text Editor's render elements.
*/
class Element {
/**
* The Text Editor plugin manager manager service.
*
* @var \Drupal\Component\Plugin\PluginManagerInterface
*/
protected $pluginManager;
/**
* Constructs a new Element object.
*
* @param \Drupal\Component\Plugin\PluginManagerInterface $plugin_manager
* The Text Editor plugin manager service.
*/
public function __construct(PluginManagerInterface $plugin_manager) {
$this->pluginManager = $plugin_manager;
}
/**
* Additional #pre_render callback for 'text_format' elements.
*/
function preRenderTextFormat(array $element) {
// Allow modules to programmatically enforce no client-side editor by
// setting the #editor property to FALSE.
if (isset($element['#editor']) && !$element['#editor']) {
return $element;
}
// filter_process_format() copies properties to the expanded 'value' child
// element, including the #pre_render property. Skip this text format
// widget, if it contains no 'format'.
if (!isset($element['format'])) {
return $element;
}
$format_ids = array_keys($element['format']['format']['#options']);
// Early-return if no text editor is associated with any of the text formats.
$editors = Editor::loadMultiple($format_ids);
if (count($editors) === 0) {
return $element;
}
// Use a hidden element for a single text format.
$field_id = $element['value']['#id'];
if (!$element['format']['format']['#access']) {
// Use the first (and only) available text format.
$format_id = $format_ids[0];
$element['format']['editor'] = array(
'#type' => 'hidden',
'#name' => $element['format']['format']['#name'],
'#value' => $format_id,
'#attributes' => array(
'class' => array('editor'),
'data-editor-for' => $field_id,
),
);
}
// Otherwise, attach to text format selector.
else {
$element['format']['format']['#attributes']['class'][] = 'editor';
$element['format']['format']['#attributes']['data-editor-for'] = $field_id;
}
// Hide the text format's filters' guidelines of those text formats that have
// a text editor associated: they're rather useless when using a text editor.
foreach ($editors as $format_id => $editor) {
$element['format']['guidelines'][$format_id]['#access'] = FALSE;
}
// Attach Text Editor module's (this module) library.
$element['#attached']['library'][] = 'editor/drupal.editor';
// Attach attachments for all available editors.
$element['#attached'] = drupal_merge_attached($element['#attached'], $this->pluginManager->getAttachments($format_ids));
// Apply XSS filters when editing content if necessary. Some types of text
// editors cannot guarantee that the end user won't become a victim of XSS.
if (!empty($element['value']['#value'])) {
$original = $element['value']['#value'];
$format = FilterFormat::load($element['format']['format']['#value']);
// Ensure XSS-safety for the current text format/editor.
$filtered = editor_filter_xss($original, $format);
if ($filtered !== FALSE) {
$element['value']['#value'] = $filtered;
}
// Only when the user has access to multiple text formats, we must add data-
// attributes for the original value and change tracking, because they are
// only necessary when the end user can switch between text formats/editors.
if ($element['format']['format']['#access']) {
$element['value']['#attributes']['data-editor-value-is-changed'] = 'false';
$element['value']['#attributes']['data-editor-value-original'] = $original;
}
}
return $element;
}
}
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment