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