From 7d1c02a73d192fb6bf8ad1d42119e691d9f55d15 Mon Sep 17 00:00:00 2001 From: Nathaniel Catchpole <catch@35733.no-reply.drupal.org> Date: Sat, 14 Jun 2014 09:46:36 +0100 Subject: [PATCH] Issue #2247779 by Wim Leers, larowlan, dawehner: Allow #pre_render, #post_render and #post_render_cache callbacks to be service methods. --- core/includes/common.inc | 40 +++++- core/modules/comment/comment.services.yml | 4 + .../comment/src/CommentPostRenderCache.php | 84 +++++++++++++ .../CommentDefaultFormatter.php | 29 +---- core/modules/editor/editor.module | 84 +------------ core/modules/editor/editor.services.yml | 3 + core/modules/editor/src/Element.php | 117 ++++++++++++++++++ 7 files changed, 249 insertions(+), 112 deletions(-) create mode 100644 core/modules/comment/src/CommentPostRenderCache.php create mode 100644 core/modules/editor/src/Element.php diff --git a/core/includes/common.inc b/core/includes/common.inc index 3f0fd1d0ce1d..ee399a473b72 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -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 diff --git a/core/modules/comment/comment.services.yml b/core/modules/comment/comment.services.yml index b0fc2ad2500b..58736341ee7f 100644 --- a/core/modules/comment/comment.services.yml +++ b/core/modules/comment/comment.services.yml @@ -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'] diff --git a/core/modules/comment/src/CommentPostRenderCache.php b/core/modules/comment/src/CommentPostRenderCache.php new file mode 100644 index 000000000000..15831e0242c2 --- /dev/null +++ b/core/modules/comment/src/CommentPostRenderCache.php @@ -0,0 +1,84 @@ +<?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; + } + +} diff --git a/core/modules/comment/src/Plugin/Field/FieldFormatter/CommentDefaultFormatter.php b/core/modules/comment/src/Plugin/Field/FieldFormatter/CommentDefaultFormatter.php index 290a607ab51d..bb17d42dad3d 100644 --- a/core/modules/comment/src/Plugin/Field/FieldFormatter/CommentDefaultFormatter.php +++ b/core/modules/comment/src/Plugin/Field/FieldFormatter/CommentDefaultFormatter.php @@ -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} */ diff --git a/core/modules/editor/editor.module b/core/modules/editor/editor.module index 00ab94cc54f2..8ff97c3a125f 100644 --- a/core/modules/editor/editor.module +++ b/core/modules/editor/editor.module @@ -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. * diff --git a/core/modules/editor/editor.services.yml b/core/modules/editor/editor.services.yml index b7acc7d2686c..731215ccbd11 100644 --- a/core/modules/editor/editor.services.yml +++ b/core/modules/editor/editor.services.yml @@ -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'] diff --git a/core/modules/editor/src/Element.php b/core/modules/editor/src/Element.php new file mode 100644 index 000000000000..f0b97bfe7d9d --- /dev/null +++ b/core/modules/editor/src/Element.php @@ -0,0 +1,117 @@ +<?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; + } + +} -- GitLab