From 5a42b47b6ed88a3bf65ff8d23ee24f49ccd20b61 Mon Sep 17 00:00:00 2001 From: catch Date: Fri, 5 Jul 2019 08:47:35 +0100 Subject: [PATCH] Issue #2966327 by alexpott, mcdruid, dww, dsnopek, catch, pwolanin, larowlan, tim.plunkett, Berdir, Sam152: Limit what can be called by a callback in render arrays to reduce the risk of RCE --- core/includes/common.inc | 93 +---------- .../Drupal/Core/Access/RouteProcessorCsrf.php | 10 +- .../Controller/EntityViewController.php | 10 +- .../Drupal/Core/Entity/EntityViewBuilder.php | 10 +- core/lib/Drupal/Core/Entity/entity.api.php | 3 +- core/lib/Drupal/Core/Form/FormBuilder.php | 10 +- .../Core/Render/Element/ElementInterface.php | 5 +- core/lib/Drupal/Core/Render/Element/Link.php | 102 ++++++++++++ .../Element/RenderCallbackInterface.php | 17 ++ core/lib/Drupal/Core/Render/Renderer.php | 60 ++++--- .../Core/Security/DoTrustedCallbackTrait.php | 103 ++++++++++++ .../Security/TrustedCallbackInterface.php | 42 +++++ .../Security/UntrustedCallbackException.php | 9 ++ core/lib/Drupal/Core/Url.php | 10 +- .../src/BigPipeRegressionTestController.php | 10 +- .../src/BigPipeTestController.php | 10 +- core/modules/block/src/BlockViewBuilder.php | 10 +- .../modules/block_test/block_test.module | 10 +- .../src/BlockRenderAlterContent.php | 20 +++ core/modules/color/color.module | 24 ++- .../src/ColorSystemBrandingBlockAlter.php | 34 ++++ .../tests/src/Kernel/ColorLegacyTest.php | 42 +++++ .../comment/src/CommentLazyBuilders.php | 13 +- .../comment/src/CommentViewBuilder.php | 2 +- core/modules/editor/src/Element.php | 10 +- core/modules/filter/filter.module | 14 +- .../modules/filter/src/Element/TextFormat.php | 22 ++- .../Plugin/Filter/FilterTestPlaceholders.php | 10 +- .../tests/src/Kernel/FilterLegacyTest.php | 30 ++++ core/modules/history/history.module | 11 +- .../history/src/HistoryRenderCallback.php | 27 ++++ .../tests/src/Kernel/HistoryLegacyTest.php | 60 +++++++ core/modules/node/src/NodeViewBuilder.php | 15 +- .../node/src/Plugin/Search/NodeSearch.php | 10 +- .../src/Form/FormTestInputForgeryForm.php | 10 +- .../src/Controller/PagerTestController.php | 10 +- ...RenderPlaceholderMessageTestController.php | 10 +- .../src/Controller/SystemTestController.php | 10 +- .../tests/src/Kernel/Theme/FunctionsTest.php | 108 ++++++++++++- .../FieldFormatter/TextTrimmedFormatter.php | 10 +- .../src/Controller/ToolbarController.php | 91 ++++++++++- .../tests/src/Kernel/ToolbarLegacyTest.php | 50 ++++++ core/modules/toolbar/toolbar.module | 80 +++------- core/modules/user/src/AccountForm.php | 10 +- .../user/src/Plugin/Block/UserLoginBlock.php | 10 +- core/modules/user/src/ToolbarLinkBuilder.php | 10 +- .../views/src/Form/ViewsFormMainForm.php | 57 ++++++- .../views/src/Plugin/views/PluginBase.php | 10 +- .../views/argument/ArgumentPluginBase.php | 9 ++ .../views/display/DisplayPluginBase.php | 9 ++ .../views/src/Plugin/views/field/Custom.php | 9 ++ .../Plugin/views/filter/FilterPluginBase.php | 9 ++ .../src/Plugin/views/sort/SortPluginBase.php | 9 ++ .../Plugin/views/style/StylePluginBase.php | 9 ++ .../Controller/ViewsTestDataController.php | 33 +++- .../views_test_data/views_test_data.module | 14 -- .../views_test_data.views_execution.inc | 10 +- .../src/Kernel/Handler/FieldKernelTest.php | 8 +- .../tests/src/Kernel/Plugin/CacheTest.php | 2 +- .../views/tests/src/Kernel/ViewsHooksTest.php | 25 ++- core/modules/views/views.module | 38 +---- .../Core/Render/RendererBubblingTest.php | 10 +- .../Core/Render/RendererCallbackTest.php | 92 +++++++++++ .../Core/Render/RendererPlaceholdersTest.php | 10 +- .../Drupal/Tests/Core/Render/RendererTest.php | 19 ++- .../Tests/Core/Render/RendererTestBase.php | 10 +- .../Security/DoTrustedCallbackTraitTest.php | 147 ++++++++++++++++++ 67 files changed, 1516 insertions(+), 300 deletions(-) create mode 100644 core/lib/Drupal/Core/Render/Element/RenderCallbackInterface.php create mode 100644 core/lib/Drupal/Core/Security/DoTrustedCallbackTrait.php create mode 100644 core/lib/Drupal/Core/Security/TrustedCallbackInterface.php create mode 100644 core/lib/Drupal/Core/Security/UntrustedCallbackException.php create mode 100644 core/modules/block/tests/modules/block_test/src/BlockRenderAlterContent.php create mode 100644 core/modules/color/src/ColorSystemBrandingBlockAlter.php create mode 100644 core/modules/color/tests/src/Kernel/ColorLegacyTest.php create mode 100644 core/modules/filter/tests/src/Kernel/FilterLegacyTest.php create mode 100644 core/modules/history/src/HistoryRenderCallback.php create mode 100644 core/modules/history/tests/src/Kernel/HistoryLegacyTest.php create mode 100644 core/modules/toolbar/tests/src/Kernel/ToolbarLegacyTest.php create mode 100644 core/tests/Drupal/Tests/Core/Render/RendererCallbackTest.php create mode 100644 core/tests/Drupal/Tests/Core/Security/DoTrustedCallbackTraitTest.php diff --git a/core/includes/common.inc b/core/includes/common.inc index c3f4190bf5e..ac78d524b93 100644 --- a/core/includes/common.inc +++ b/core/includes/common.inc @@ -16,7 +16,6 @@ use Drupal\Component\Utility\Html; use Drupal\Component\Utility\SortArray; use Drupal\Component\Utility\UrlHelper; use Drupal\Core\Cache\Cache; -use Drupal\Core\Render\BubbleableMetadata; use Drupal\Core\Render\Element; use Drupal\Core\Render\Element\Link; use Drupal\Core\Render\HtmlResponseAttachmentsProcessor; @@ -777,96 +776,14 @@ function drupal_pre_render_link($element) { /** * Pre-render callback: Collects child links into a single array. * - * This function can be added as a pre_render callback for a renderable array, - * usually one which will be themed by links.html.twig. It iterates through all - * unrendered children of the element, collects any #links properties it finds, - * merges them into the parent element's #links array, and prevents those - * children from being rendered separately. + * @deprecated in Drupal 8.8.0 and will be removed before Drupal 9.0.0. Use + * \Drupal\Core\Render\Element\Link::preRenderLinks() instead. * - * The purpose of this is to allow links to be logically grouped into related - * categories, so that each child group can be rendered as its own list of - * links if drupal_render() is called on it, but calling drupal_render() on the - * parent element will still produce a single list containing all the remaining - * links, regardless of what group they were in. - * - * A typical example comes from node links, which are stored in a renderable - * array similar to this: - * @code - * $build['links'] = array( - * '#theme' => 'links__node', - * '#pre_render' => array('drupal_pre_render_links'), - * 'comment' => array( - * '#theme' => 'links__node__comment', - * '#links' => array( - * // An array of links associated with node comments, suitable for - * // passing in to links.html.twig. - * ), - * ), - * 'statistics' => array( - * '#theme' => 'links__node__statistics', - * '#links' => array( - * // An array of links associated with node statistics, suitable for - * // passing in to links.html.twig. - * ), - * ), - * 'translation' => array( - * '#theme' => 'links__node__translation', - * '#links' => array( - * // An array of links associated with node translation, suitable for - * // passing in to links.html.twig. - * ), - * ), - * ); - * @endcode - * - * In this example, the links are grouped by functionality, which can be - * helpful to themers who want to display certain kinds of links independently. - * For example, adding this code to node.html.twig will result in the comment - * links being rendered as a single list: - * @code - * {{ content.links.comment }} - * @endcode - * - * (where a node's content has been transformed into $content before handing - * control to the node.html.twig template). - * - * The pre_render function defined here allows the above flexibility, but also - * allows the following code to be used to render all remaining links into a - * single list, regardless of their group: - * @code - * {{ content.links }} - * @endcode - * - * In the above example, this will result in the statistics and translation - * links being rendered together in a single list (but not the comment links, - * which were rendered previously on their own). - * - * Because of the way this function works, the individual properties of each - * group (for example, a group-specific #theme property such as - * 'links__node__comment' in the example above, or any other property such as - * #attributes or #pre_render that is attached to it) are only used when that - * group is rendered on its own. When the group is rendered together with other - * children, these child-specific properties are ignored, and only the overall - * properties of the parent are used. + * @see https://www.drupal.org/node/2966725 */ function drupal_pre_render_links($element) { - $element += ['#links' => [], '#attached' => []]; - foreach (Element::children($element) as $key) { - $child = &$element[$key]; - // If the child has links which have not been printed yet and the user has - // access to it, merge its links in to the parent. - if (isset($child['#links']) && empty($child['#printed']) && Element::isVisibleElement($child)) { - $element['#links'] += $child['#links']; - // Mark the child as having been printed already (so that its links - // cannot be mistakenly rendered twice). - $child['#printed'] = TRUE; - } - // Merge attachments. - if (isset($child['#attached'])) { - $element['#attached'] = BubbleableMetadata::mergeAttachments($element['#attached'], $child['#attached']); - } - } - return $element; + @trigger_error('drupal_pre_render_links() is deprecated in Drupal 8.8.0 and will be removed before Drupal 9.0.0. Use \Drupal\Core\Render\Element\Link::preRenderLinks() instead. See https://www.drupal.org/node/2966725', E_USER_DEPRECATED); + return Link::preRenderLinks($element); } /** diff --git a/core/lib/Drupal/Core/Access/RouteProcessorCsrf.php b/core/lib/Drupal/Core/Access/RouteProcessorCsrf.php index 9f2e7d40927..d0aec301855 100644 --- a/core/lib/Drupal/Core/Access/RouteProcessorCsrf.php +++ b/core/lib/Drupal/Core/Access/RouteProcessorCsrf.php @@ -4,13 +4,14 @@ namespace Drupal\Core\Access; use Drupal\Component\Utility\Crypt; use Drupal\Core\Render\BubbleableMetadata; +use Drupal\Core\Security\TrustedCallbackInterface; use Drupal\Core\RouteProcessor\OutboundRouteProcessorInterface; use Symfony\Component\Routing\Route; /** * Processes the outbound route to handle the CSRF token. */ -class RouteProcessorCsrf implements OutboundRouteProcessorInterface { +class RouteProcessorCsrf implements OutboundRouteProcessorInterface, TrustedCallbackInterface { /** * The CSRF token generator. @@ -81,4 +82,11 @@ class RouteProcessorCsrf implements OutboundRouteProcessorInterface { ]; } + /** + * {@inheritdoc} + */ + public static function trustedCallbacks() { + return ['renderPlaceholderCsrfToken']; + } + } diff --git a/core/lib/Drupal/Core/Entity/Controller/EntityViewController.php b/core/lib/Drupal/Core/Entity/Controller/EntityViewController.php index 274c47ad960..f762333f4a2 100644 --- a/core/lib/Drupal/Core/Entity/Controller/EntityViewController.php +++ b/core/lib/Drupal/Core/Entity/Controller/EntityViewController.php @@ -7,13 +7,14 @@ use Drupal\Core\DependencyInjection\DeprecatedServicePropertyTrait; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Entity\FieldableEntityInterface; +use Drupal\Core\Security\TrustedCallbackInterface; use Drupal\Core\Render\RendererInterface; use Symfony\Component\DependencyInjection\ContainerInterface; /** * Defines a generic controller to render a single entity. */ -class EntityViewController implements ContainerInjectionInterface { +class EntityViewController implements ContainerInjectionInterface, TrustedCallbackInterface { use DeprecatedServicePropertyTrait; /** @@ -110,6 +111,13 @@ class EntityViewController implements ContainerInjectionInterface { return $page; } + /** + * {@inheritdoc} + */ + public static function trustedCallbacks() { + return ['buildTitle']; + } + /** * Provides a page to render a single entity revision. * diff --git a/core/lib/Drupal/Core/Entity/EntityViewBuilder.php b/core/lib/Drupal/Core/Entity/EntityViewBuilder.php index 685fa38904f..5c25721e9cc 100644 --- a/core/lib/Drupal/Core/Entity/EntityViewBuilder.php +++ b/core/lib/Drupal/Core/Entity/EntityViewBuilder.php @@ -11,6 +11,7 @@ use Drupal\Core\Field\FieldItemInterface; use Drupal\Core\Field\FieldItemListInterface; use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\Render\Element; +use Drupal\Core\Security\TrustedCallbackInterface; use Drupal\Core\Theme\Registry; use Drupal\Core\TypedData\TranslatableInterface as TranslatableDataInterface; use Symfony\Component\DependencyInjection\ContainerInterface; @@ -20,7 +21,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface; * * @ingroup entity_api */ -class EntityViewBuilder extends EntityHandlerBase implements EntityHandlerInterface, EntityViewBuilderInterface { +class EntityViewBuilder extends EntityHandlerBase implements EntityHandlerInterface, EntityViewBuilderInterface, TrustedCallbackInterface { use DeprecatedServicePropertyTrait; /** @@ -142,6 +143,13 @@ class EntityViewBuilder extends EntityHandlerBase implements EntityHandlerInterf return $build; } + /** + * {@inheritdoc} + */ + public static function trustedCallbacks() { + return ['build', 'buildMultiple']; + } + /** * {@inheritdoc} */ diff --git a/core/lib/Drupal/Core/Entity/entity.api.php b/core/lib/Drupal/Core/Entity/entity.api.php index 824c894657e..f557458d380 100644 --- a/core/lib/Drupal/Core/Entity/entity.api.php +++ b/core/lib/Drupal/Core/Entity/entity.api.php @@ -1530,7 +1530,8 @@ function hook_entity_view_alter(array &$build, Drupal\Core\Entity\EntityInterfac $build['an_additional_field']['#weight'] = -10; // Add a #post_render callback to act on the rendered HTML of the entity. - $build['#post_render'][] = 'my_module_node_post_render'; + // The object must implement \Drupal\Core\Security\TrustedCallbackInterface. + $build['#post_render'][] = '\Drupal\my_module\NodeCallback::postRender'; } } diff --git a/core/lib/Drupal/Core/Form/FormBuilder.php b/core/lib/Drupal/Core/Form/FormBuilder.php index df716e3f887..d6e3bebd0e8 100644 --- a/core/lib/Drupal/Core/Form/FormBuilder.php +++ b/core/lib/Drupal/Core/Form/FormBuilder.php @@ -15,6 +15,7 @@ use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Form\Exception\BrokenPostRequestException; use Drupal\Core\Render\Element; use Drupal\Core\Render\ElementInfoManagerInterface; +use Drupal\Core\Security\TrustedCallbackInterface; use Drupal\Core\Theme\ThemeManagerInterface; use Symfony\Component\EventDispatcher\EventDispatcherInterface; use Symfony\Component\HttpFoundation\FileBag; @@ -26,7 +27,7 @@ use Symfony\Component\HttpFoundation\Response; * * @ingroup form_api */ -class FormBuilder implements FormBuilderInterface, FormValidatorInterface, FormSubmitterInterface, FormCacheInterface { +class FormBuilder implements FormBuilderInterface, FormValidatorInterface, FormSubmitterInterface, FormCacheInterface, TrustedCallbackInterface { /** * The module handler. @@ -1406,4 +1407,11 @@ class FormBuilder implements FormBuilderInterface, FormValidatorInterface, FormS return $this->currentUser; } + /** + * {@inheritdoc} + */ + public static function trustedCallbacks() { + return ['renderPlaceholderFormAction', 'renderFormTokenPlaceholder']; + } + } diff --git a/core/lib/Drupal/Core/Render/Element/ElementInterface.php b/core/lib/Drupal/Core/Render/Element/ElementInterface.php index 12cb67d3a14..97cd47827fe 100644 --- a/core/lib/Drupal/Core/Render/Element/ElementInterface.php +++ b/core/lib/Drupal/Core/Render/Element/ElementInterface.php @@ -17,6 +17,9 @@ use Drupal\Component\Plugin\PluginInspectionInterface; * Some render elements are specifically form input elements; see * \Drupal\Core\Render\Element\FormElementInterface for more information. * + * The public API of these objects must be designed with security in mind as + * render elements process raw user input. + * * @see \Drupal\Core\Render\ElementInfoManager * @see \Drupal\Core\Render\Annotation\RenderElement * @see \Drupal\Core\Render\Element\RenderElement @@ -24,7 +27,7 @@ use Drupal\Component\Plugin\PluginInspectionInterface; * * @ingroup theme_render */ -interface ElementInterface extends PluginInspectionInterface { +interface ElementInterface extends PluginInspectionInterface, RenderCallbackInterface { /** * Returns the element properties for this element. diff --git a/core/lib/Drupal/Core/Render/Element/Link.php b/core/lib/Drupal/Core/Render/Element/Link.php index 25edc42c981..49fc836fea7 100644 --- a/core/lib/Drupal/Core/Render/Element/Link.php +++ b/core/lib/Drupal/Core/Render/Element/Link.php @@ -5,6 +5,7 @@ namespace Drupal\Core\Render\Element; use Drupal\Component\Utility\NestedArray; use Drupal\Component\Utility\Html as HtmlUtility; use Drupal\Core\Render\BubbleableMetadata; +use Drupal\Core\Render\Element; use Drupal\Core\Url as CoreUrl; /** @@ -99,4 +100,105 @@ class Link extends RenderElement { return $element; } + /** + * Pre-render callback: Collects child links into a single array. + * + * This method can be added as a pre_render callback for a renderable array, + * usually one which will be themed by links.html.twig. It iterates through + * all unrendered children of the element, collects any #links properties it + * finds, merges them into the parent element's #links array, and prevents + * those children from being rendered separately. + * + * The purpose of this is to allow links to be logically grouped into related + * categories, so that each child group can be rendered as its own list of + * links if drupal_render() is called on it, but calling drupal_render() on + * the parent element will still produce a single list containing all the + * remaining links, regardless of what group they were in. + * + * A typical example comes from node links, which are stored in a renderable + * array similar to this: + * @code + * $build['links'] = array( + * '#theme' => 'links__node', + * '#pre_render' => array(Link::class, 'preRenderLinks'), + * 'comment' => array( + * '#theme' => 'links__node__comment', + * '#links' => array( + * // An array of links associated with node comments, suitable for + * // passing in to links.html.twig. + * ), + * ), + * 'statistics' => array( + * '#theme' => 'links__node__statistics', + * '#links' => array( + * // An array of links associated with node statistics, suitable for + * // passing in to links.html.twig. + * ), + * ), + * 'translation' => array( + * '#theme' => 'links__node__translation', + * '#links' => array( + * // An array of links associated with node translation, suitable for + * // passing in to links.html.twig. + * ), + * ), + * ); + * @endcode + * + * In this example, the links are grouped by functionality, which can be + * helpful to themers who want to display certain kinds of links + * independently. For example, adding this code to node.html.twig will result + * in the comment links being rendered as a single list: + * @code + * {{ content.links.comment }} + * @endcode + * + * (where a node's content has been transformed into $content before handing + * control to the node.html.twig template). + * + * The preRenderLinks method defined here allows the above flexibility, but + * also allows the following code to be used to render all remaining links + * into a single list, regardless of their group: + * @code + * {{ content.links }} + * @endcode + * + * In the above example, this will result in the statistics and translation + * links being rendered together in a single list (but not the comment links, + * which were rendered previously on their own). + * + * Because of the way this method works, the individual properties of each + * group (for example, a group-specific #theme property such as + * 'links__node__comment' in the example above, or any other property such as + * #attributes or #pre_render that is attached to it) are only used when that + * group is rendered on its own. When the group is rendered together with + * other children, these child-specific properties are ignored, and only the + * overall properties of the parent are used. + * + * @param array $element + * Render array containing child links to group. + * + * @return array + * Render array containing child links grouped into a single array. + */ + public static function preRenderLinks($element) { + $element += ['#links' => [], '#attached' => []]; + foreach (Element::children($element) as $key) { + $child = &$element[$key]; + // If the child has links which have not been printed yet and the user has + // access to it, merge its links in to the parent. + if (isset($child['#links']) && empty($child['#printed']) && Element::isVisibleElement($child)) { + $element['#links'] += $child['#links']; + // Mark the child as having been printed already (so that its links + // cannot be mistakenly rendered twice). + $child['#printed'] = TRUE; + } + // Merge attachments. + if (isset($child['#attached'])) { + $element['#attached'] = BubbleableMetadata::mergeAttachments($element['#attached'], $child['#attached']); + } + } + return $element; + } + } diff --git a/core/lib/Drupal/Core/Render/Element/RenderCallbackInterface.php b/core/lib/Drupal/Core/Render/Element/RenderCallbackInterface.php new file mode 100644 index 00000000000..b244f510fbc --- /dev/null +++ b/core/lib/Drupal/Core/Render/Element/RenderCallbackInterface.php @@ -0,0 +1,17 @@ +controllerResolver->getControllerFromDefinition($elements['#access_callback']); - } - $elements['#access'] = call_user_func($elements['#access_callback'], $elements); + $elements['#access'] = $this->doCallback('#access_callback', $elements['#access_callback'], [$elements]); } // Early-return nothing if user does not have access. @@ -350,12 +351,7 @@ class Renderer implements RendererInterface { } // Build the element if it is still empty. if (isset($elements['#lazy_builder'])) { - $callable = $elements['#lazy_builder'][0]; - $args = $elements['#lazy_builder'][1]; - if (is_string($callable) && strpos($callable, '::') === FALSE) { - $callable = $this->controllerResolver->getControllerFromDefinition($callable); - } - $new_elements = call_user_func_array($callable, $args); + $new_elements = $this->doCallback('#lazy_builder', $elements['#lazy_builder'][0], $elements['#lazy_builder'][1]); // Retain the original cacheability metadata, plus cache keys. CacheableMetadata::createFromRenderArray($elements) ->merge(CacheableMetadata::createFromRenderArray($new_elements)) @@ -372,10 +368,7 @@ class Renderer implements RendererInterface { // element is rendered into the final text. if (isset($elements['#pre_render'])) { foreach ($elements['#pre_render'] as $callable) { - if (is_string($callable) && strpos($callable, '::') === FALSE) { - $callable = $this->controllerResolver->getControllerFromDefinition($callable); - } - $elements = call_user_func($callable, $elements); + $elements = $this->doCallback('#pre_render', $callable, [$elements]); } } @@ -499,10 +492,7 @@ class Renderer implements RendererInterface { // outputted text to be filtered. if (isset($elements['#post_render'])) { foreach ($elements['#post_render'] as $callable) { - if (is_string($callable) && strpos($callable, '::') === FALSE) { - $callable = $this->controllerResolver->getControllerFromDefinition($callable); - } - $elements['#children'] = call_user_func($callable, $elements['#children'], $elements); + $elements['#children'] = $this->doCallback('#post_render', $callable, [$elements['#children'], $elements]); } } @@ -756,4 +746,38 @@ class Renderer implements RendererInterface { return $elements; } + /** + * Performs a callback. + * + * @param string $callback_type + * The type of the callback. For example, '#post_render'. + * @param string|callable $callback + * The callback to perform. + * @param array $args + * The arguments to pass to the callback. + * + * @return mixed + * The callback's return value. + * + * @see \Drupal\Core\Security\TrustedCallbackInterface + */ + protected function doCallback($callback_type, $callback, array $args) { + if (is_string($callback)) { + $double_colon = strpos($callback, '::'); + if ($double_colon === FALSE) { + $callback = $this->controllerResolver->getControllerFromDefinition($callback); + } + elseif ($double_colon > 0) { + $callback = explode('::', $callback, 2); + } + } + $message = sprintf('Render %s callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. Support for this callback implementation is deprecated in 8.8.0 and will be removed in Drupal 9.0.0. See https://www.drupal.org/node/2966725', $callback_type, '%s'); + // Add \Drupal\Core\Render\Element\RenderCallbackInterface as an extra + // trusted interface so that: + // - All public methods on Render elements are considered trusted. + // - Helper classes that contain only callback methods can implement this + // instead of TrustedCallbackInterface. + return $this->doTrustedCallback($callback, $args, $message, TrustedCallbackInterface::TRIGGER_SILENCED_DEPRECATION, RenderCallbackInterface::class); + } + } diff --git a/core/lib/Drupal/Core/Security/DoTrustedCallbackTrait.php b/core/lib/Drupal/Core/Security/DoTrustedCallbackTrait.php new file mode 100644 index 00000000000..d3d86b977ba --- /dev/null +++ b/core/lib/Drupal/Core/Security/DoTrustedCallbackTrait.php @@ -0,0 +1,103 @@ +trustedCallbacks(); + } + else { + $methods = call_user_func($object_or_classname . '::trustedCallbacks'); + } + $safe_callback = in_array($method_name, $methods, TRUE); + } + } + elseif ($callback instanceof \Closure) { + $safe_callback = TRUE; + } + + if (!$safe_callback) { + $description = $object_or_classname; + if (is_object($description)) { + $description = get_class($description); + } + if (isset($method_name)) { + $description .= '::' . $method_name; + } + $message = sprintf($message, $description); + if ($error_type === TrustedCallbackInterface::TRIGGER_SILENCED_DEPRECATION) { + @trigger_error($message, E_USER_DEPRECATED); + } + elseif ($error_type === TrustedCallbackInterface::TRIGGER_WARNING) { + trigger_error($message, E_USER_WARNING); + } + else { + throw new UntrustedCallbackException($message); + } + } + + return call_user_func_array($callback, $args); + } + +} diff --git a/core/lib/Drupal/Core/Security/TrustedCallbackInterface.php b/core/lib/Drupal/Core/Security/TrustedCallbackInterface.php new file mode 100644 index 00000000000..78f6bb93439 --- /dev/null +++ b/core/lib/Drupal/Core/Security/TrustedCallbackInterface.php @@ -0,0 +1,42 @@ +var hitsTheFloor = "";'; @@ -43,4 +44,11 @@ class BigPipeRegressionTestController { ]; } + /** + * {@inheritdoc} + */ + public static function trustedCallbacks() { + return ['currentTime']; + } + } diff --git a/core/modules/big_pipe/tests/modules/big_pipe_test/src/BigPipeTestController.php b/core/modules/big_pipe/tests/modules/big_pipe_test/src/BigPipeTestController.php index baa5f708f05..19d85f05989 100644 --- a/core/modules/big_pipe/tests/modules/big_pipe_test/src/BigPipeTestController.php +++ b/core/modules/big_pipe/tests/modules/big_pipe_test/src/BigPipeTestController.php @@ -4,8 +4,9 @@ namespace Drupal\big_pipe_test; use Drupal\big_pipe\Render\BigPipeMarkup; use Drupal\big_pipe_test\EventSubscriber\BigPipeTestSubscriber; +use Drupal\Core\Security\TrustedCallbackInterface; -class BigPipeTestController { +class BigPipeTestController implements TrustedCallbackInterface { /** * Returns a all BigPipe placeholder test case render arrays. @@ -157,4 +158,11 @@ class BigPipeTestController { ]; } + /** + * {@inheritdoc} + */ + public static function trustedCallbacks() { + return ['currentTime', 'counter']; + } + } diff --git a/core/modules/block/src/BlockViewBuilder.php b/core/modules/block/src/BlockViewBuilder.php index 72f3b1e5165..d8d2f23ff8b 100644 --- a/core/modules/block/src/BlockViewBuilder.php +++ b/core/modules/block/src/BlockViewBuilder.php @@ -12,11 +12,12 @@ use Drupal\Core\Extension\ModuleHandlerInterface; use Drupal\Core\Plugin\ContextAwarePluginInterface; use Drupal\Core\Render\Element; use Drupal\block\Entity\Block; +use Drupal\Core\Security\TrustedCallbackInterface; /** * Provides a Block view builder. */ -class BlockViewBuilder extends EntityViewBuilder { +class BlockViewBuilder extends EntityViewBuilder implements TrustedCallbackInterface { /** * {@inheritdoc} @@ -135,6 +136,13 @@ class BlockViewBuilder extends EntityViewBuilder { return $build; } + /** + * {@inheritdoc} + */ + public static function trustedCallbacks() { + return ['preRender', 'lazyBuilder']; + } + /** * #lazy_builder callback; builds a #pre_render-able block. * diff --git a/core/modules/block/tests/modules/block_test/block_test.module b/core/modules/block/tests/modules/block_test/block_test.module index 7ea8bb4d6f8..b6f171541e6 100644 --- a/core/modules/block/tests/modules/block_test/block_test.module +++ b/core/modules/block/tests/modules/block_test/block_test.module @@ -25,7 +25,7 @@ function block_test_block_view_test_cache_alter(array &$build, BlockPluginInterf $build['#attributes']['foo'] = 'bar'; } if (\Drupal::state()->get('block_test_view_alter_append_pre_render_prefix') !== NULL) { - $build['#pre_render'][] = 'block_test_pre_render_alter_content'; + $build['#pre_render'][] = '\Drupal\block_test\BlockRenderAlterContent::preRender'; } } @@ -52,11 +52,3 @@ function block_test_block_build_test_cache_alter(array &$build, BlockPluginInter $build['#create_placeholder'] = \Drupal::state()->get('block_test_block_alter_create_placeholder'); } } - -/** - * #pre_render callback for a block to alter its content. - */ -function block_test_pre_render_alter_content($build) { - $build['#prefix'] = 'Hiya!
'; - return $build; -} diff --git a/core/modules/block/tests/modules/block_test/src/BlockRenderAlterContent.php b/core/modules/block/tests/modules/block_test/src/BlockRenderAlterContent.php new file mode 100644 index 00000000000..c52a581d81b --- /dev/null +++ b/core/modules/block/tests/modules/block_test/src/BlockRenderAlterContent.php @@ -0,0 +1,20 @@ +'; + return $build; + } + +} diff --git a/core/modules/color/color.module b/core/modules/color/color.module index 5f1d9375e6f..48479e8773a 100644 --- a/core/modules/color/color.module +++ b/core/modules/color/color.module @@ -11,13 +11,13 @@ use Drupal\Component\Utility\Color; use Drupal\Component\Utility\Environment; use Drupal\Core\Asset\CssOptimizer; use Drupal\Core\Block\BlockPluginInterface; -use Drupal\Core\Cache\CacheableMetadata; use Drupal\Core\File\Exception\FileException; use Drupal\Core\File\FileSystemInterface; use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Render\Element\Textfield; use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\color\ColorSystemBrandingBlockAlter; /** * Implements hook_help(). @@ -116,26 +116,20 @@ function color_library_info_alter(&$libraries, $extension) { * Implements hook_block_view_BASE_BLOCK_ID_alter(). */ function color_block_view_system_branding_block_alter(array &$build, BlockPluginInterface $block) { - $build['#pre_render'][] = 'color_block_view_pre_render'; + $build['#pre_render'][] = [ColorSystemBrandingBlockAlter::class, 'preRender']; } /** * #pre_render callback: Sets color preset logo. + * + * @deprecated in Drupal 8.8.0 and will be removed before Drupal 9.0.0. Use + * \Drupal\color\ColorSystemBrandingBlockAlter::preRender() instead. + * + * @see https://www.drupal.org/node/2966725 */ function color_block_view_pre_render(array $build) { - $theme_key = \Drupal::theme()->getActiveTheme()->getName(); - $config = \Drupal::config('color.theme.' . $theme_key); - CacheableMetadata::createFromRenderArray($build) - ->addCacheableDependency($config) - ->applyTo($build); - - // Override logo. - $logo = $config->get('logo'); - if ($logo && $build['content']['site_logo'] && preg_match('!' . $theme_key . '/logo.svg$!', $build['content']['site_logo']['#uri'])) { - $build['content']['site_logo']['#uri'] = file_url_transform_relative(file_create_url($logo)); - } - - return $build; + @trigger_error('color_block_view_pre_render() is deprecated in Drupal 8.8.0 and will be removed before Drupal 9.0.0. Use \Drupal\color\ColorSystemBrandingBlockAlter::preRender() instead. See https://www.drupal.org/node/2966725', E_USER_DEPRECATED); + return ColorSystemBrandingBlockAlter::preRender($build); } /** diff --git a/core/modules/color/src/ColorSystemBrandingBlockAlter.php b/core/modules/color/src/ColorSystemBrandingBlockAlter.php new file mode 100644 index 00000000000..6408e1e71f7 --- /dev/null +++ b/core/modules/color/src/ColorSystemBrandingBlockAlter.php @@ -0,0 +1,34 @@ +getActiveTheme()->getName(); + $config = \Drupal::config('color.theme.' . $theme_key); + CacheableMetadata::createFromRenderArray($build) + ->addCacheableDependency($config) + ->applyTo($build); + + // Override logo. + $logo = $config->get('logo'); + if ($logo && $build['content']['site_logo'] && preg_match('!' . $theme_key . '/logo.svg$!', $build['content']['site_logo']['#uri'])) { + $build['content']['site_logo']['#uri'] = file_url_transform_relative(file_create_url($logo)); + } + + return $build; + } + +} diff --git a/core/modules/color/tests/src/Kernel/ColorLegacyTest.php b/core/modules/color/tests/src/Kernel/ColorLegacyTest.php new file mode 100644 index 00000000000..07eaaa7cb4d --- /dev/null +++ b/core/modules/color/tests/src/Kernel/ColorLegacyTest.php @@ -0,0 +1,42 @@ +container->get('theme_installer')->install(['bartik']); + $this->config('system.theme') + ->set('default', 'bartik') + ->save(); + } + + /** + * Tests color_block_view_pre_render() deprecation. + * + * @expectedDeprecation color_block_view_pre_render() is deprecated in Drupal 8.8.0 and will be removed before Drupal 9.0.0. Use \Drupal\color\ColorSystemBrandingBlockAlter::preRender() instead. See https://www.drupal.org/node/2966725 + */ + public function testColorSystemBrandingBlockAlterPreRender() { + $render = color_block_view_pre_render([]); + $this->assertEquals(['config:color.theme.bartik'], $render['#cache']['tags']); + } + +} diff --git a/core/modules/comment/src/CommentLazyBuilders.php b/core/modules/comment/src/CommentLazyBuilders.php index 22eaa939dcd..455bb689ea2 100644 --- a/core/modules/comment/src/CommentLazyBuilders.php +++ b/core/modules/comment/src/CommentLazyBuilders.php @@ -8,6 +8,8 @@ use Drupal\Core\Entity\EntityFormBuilderInterface; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityTypeManagerInterface; use Drupal\Core\Extension\ModuleHandlerInterface; +use Drupal\Core\Render\Element\Link; +use Drupal\Core\Security\TrustedCallbackInterface; use Drupal\Core\Render\RendererInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Url; @@ -15,7 +17,7 @@ use Drupal\Core\Url; /** * Defines a service for comment #lazy_builder callbacks. */ -class CommentLazyBuilders { +class CommentLazyBuilders implements TrustedCallbackInterface { use DeprecatedServicePropertyTrait; /** @@ -135,7 +137,7 @@ class CommentLazyBuilders { public function renderLinks($comment_entity_id, $view_mode, $langcode, $is_in_preview) { $links = [ '#theme' => 'links__comment', - '#pre_render' => ['drupal_pre_render_links'], + '#pre_render' => [[Link::class, 'preRenderLinks']], '#attributes' => ['class' => ['links', 'inline']], ]; @@ -231,4 +233,11 @@ class CommentLazyBuilders { return content_translation_translate_access($entity); } + /** + * {@inheritdoc} + */ + public static function trustedCallbacks() { + return ['renderLinks', 'renderForm']; + } + } diff --git a/core/modules/comment/src/CommentViewBuilder.php b/core/modules/comment/src/CommentViewBuilder.php index e776ad70ea9..b3fd17e59c3 100644 --- a/core/modules/comment/src/CommentViewBuilder.php +++ b/core/modules/comment/src/CommentViewBuilder.php @@ -169,7 +169,7 @@ class CommentViewBuilder extends EntityViewBuilder { // Embed the metadata for the comment "new" indicators on this node. $build[$id]['history'] = [ - '#lazy_builder' => ['history_attach_timestamp', [$commented_entity->id()]], + '#lazy_builder' => ['\Drupal\history\HistoryRenderCallback::lazyBuilder', [$commented_entity->id()]], '#create_placeholder' => TRUE, ]; } diff --git a/core/modules/editor/src/Element.php b/core/modules/editor/src/Element.php index 9f8ab916141..52db7683831 100644 --- a/core/modules/editor/src/Element.php +++ b/core/modules/editor/src/Element.php @@ -2,6 +2,7 @@ namespace Drupal\editor; +use Drupal\Core\Security\TrustedCallbackInterface; use Drupal\editor\Entity\Editor; use Drupal\filter\Entity\FilterFormat; use Drupal\Component\Plugin\PluginManagerInterface; @@ -10,7 +11,7 @@ use Drupal\Core\Render\BubbleableMetadata; /** * Defines a service for Text Editor's render elements. */ -class Element { +class Element implements TrustedCallbackInterface { /** * The Text Editor plugin manager service. @@ -29,6 +30,13 @@ class Element { $this->pluginManager = $plugin_manager; } + /** + * {@inheritdoc} + */ + public static function trustedCallbacks() { + return ['preRenderTextFormat']; + } + /** * Additional #pre_render callback for 'text_format' elements. */ diff --git a/core/modules/filter/filter.module b/core/modules/filter/filter.module index 44dc48018c5..393ef255d57 100644 --- a/core/modules/filter/filter.module +++ b/core/modules/filter/filter.module @@ -12,6 +12,7 @@ use Drupal\Core\Cache\Cache; use Drupal\Core\Routing\RouteMatchInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Template\Attribute; +use Drupal\filter\Element\TextFormat; use Drupal\filter\FilterFormatInterface; /** @@ -306,17 +307,14 @@ function check_markup($text, $format_id = NULL, $langcode = '', $filter_types_to /** * Render API callback: Hides the field value of 'text_format' elements. * - * To not break form processing and previews if a user does not have access to - * a stored text format, the expanded form elements in filter_process_format() - * are forced to take over the stored #default_values for 'value' and 'format'. - * However, to prevent the unfiltered, original #value from being displayed to - * the user, we replace it with a friendly notice here. + * @deprecated in Drupal 8.8.0 and will be removed before Drupal 9.0.0. Use + * \Drupal\filter\Element\TextFormat::accessDeniedCallback() instead. * - * @see filter_process_format() + * @see https://www.drupal.org/node/2966725 */ function filter_form_access_denied($element) { - $element['#value'] = t('This field has been disabled because you do not have sufficient permissions to edit it.'); - return $element; + @trigger_error('filter_form_access_denied() is deprecated in Drupal 8.8.0 and will be removed before Drupal 9.0.0. Use \Drupal\filter\Element\TextFormat::accessDeniedCallback() instead. See https://www.drupal.org/node/2966725', E_USER_DEPRECATED); + return TextFormat::accessDeniedCallback($element); } /** diff --git a/core/modules/filter/src/Element/TextFormat.php b/core/modules/filter/src/Element/TextFormat.php index b0fc947b75f..cfdce44b24b 100644 --- a/core/modules/filter/src/Element/TextFormat.php +++ b/core/modules/filter/src/Element/TextFormat.php @@ -226,7 +226,7 @@ class TextFormat extends RenderElement { // Prepend #pre_render callback to replace field value with user notice // prior to rendering. $element['value'] += ['#pre_render' => []]; - array_unshift($element['value']['#pre_render'], 'filter_form_access_denied'); + array_unshift($element['value']['#pre_render'], [static::class, 'accessDeniedCallback']); // Cosmetic adjustments. if (isset($element['value']['#rows'])) { @@ -247,6 +247,26 @@ class TextFormat extends RenderElement { return $element; } + /** + * Render API callback: Hides the field value of 'text_format' elements. + * + * To not break form processing and previews if a user does not have access to + * a stored text format, the expanded form elements in filter_process_format() + * are forced to take over the stored #default_values for 'value' and + * 'format'. However, to prevent the unfiltered, original #value from being + * displayed to the user, we replace it with a friendly notice here. + * + * @param array $element + * The render array to add the access denied message to. + * + * @return array + * The updated render array. + */ + public static function accessDeniedCallback(array $element) { + $element['#value'] = t('This field has been disabled because you do not have sufficient permissions to edit it.'); + return $element; + } + /** * Wraps the current user. * diff --git a/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestPlaceholders.php b/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestPlaceholders.php index c6d03000f98..f4745bb5fbf 100644 --- a/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestPlaceholders.php +++ b/core/modules/filter/tests/filter_test/src/Plugin/Filter/FilterTestPlaceholders.php @@ -2,6 +2,7 @@ namespace Drupal\filter_test\Plugin\Filter; +use Drupal\Core\Security\TrustedCallbackInterface; use Drupal\filter\FilterProcessResult; use Drupal\filter\Plugin\FilterBase; @@ -15,7 +16,7 @@ use Drupal\filter\Plugin\FilterBase; * type = Drupal\filter\Plugin\FilterInterface::TYPE_TRANSFORM_REVERSIBLE * ) */ -class FilterTestPlaceholders extends FilterBase { +class FilterTestPlaceholders extends FilterBase implements TrustedCallbackInterface { /** * {@inheritdoc} @@ -42,4 +43,11 @@ class FilterTestPlaceholders extends FilterBase { ]; } + /** + * {@inheritdoc} + */ + public static function trustedCallbacks() { + return ['renderDynamicThing']; + } + } diff --git a/core/modules/filter/tests/src/Kernel/FilterLegacyTest.php b/core/modules/filter/tests/src/Kernel/FilterLegacyTest.php new file mode 100644 index 00000000000..f017b6b321f --- /dev/null +++ b/core/modules/filter/tests/src/Kernel/FilterLegacyTest.php @@ -0,0 +1,30 @@ +assertEquals('This field has been disabled because you do not have sufficient permissions to edit it.', $element['#value']); + } + +} diff --git a/core/modules/history/history.module b/core/modules/history/history.module index fc64961f49e..3f73c2dc028 100644 --- a/core/modules/history/history.module +++ b/core/modules/history/history.module @@ -13,6 +13,7 @@ use Drupal\Core\Url; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\Display\EntityViewDisplayInterface; use Drupal\Core\Routing\RouteMatchInterface; +use Drupal\history\HistoryRenderCallback; use Drupal\user\UserInterface; /** @@ -188,9 +189,13 @@ function history_user_delete($account) { * * @return array * A renderable array containing the last read timestamp. + * + * @deprecated in Drupal 8.8.0 and will be removed before Drupal 9.0.0. Use + * \Drupal\history\HistoryRenderCallback::lazyBuilder() instead. + * + * @see https://www.drupal.org/node/2966725 */ function history_attach_timestamp($node_id) { - $element = []; - $element['#attached']['drupalSettings']['history']['lastReadTimestamps'][$node_id] = (int) history_read($node_id); - return $element; + @trigger_error('history_attach_timestamp() is deprecated in Drupal 8.8.0 and will be removed before Drupal 9.0.0. Use \Drupal\history\HistoryRenderCallback::lazyBuilder() instead. See https://www.drupal.org/node/2966725', E_USER_DEPRECATED); + return HistoryRenderCallback::lazyBuilder($node_id); } diff --git a/core/modules/history/src/HistoryRenderCallback.php b/core/modules/history/src/HistoryRenderCallback.php new file mode 100644 index 00000000000..c6a92eda68b --- /dev/null +++ b/core/modules/history/src/HistoryRenderCallback.php @@ -0,0 +1,27 @@ +installEntitySchema('node'); + $this->installEntitySchema('user'); + $this->installSchema('history', ['history']); + $this->installSchema('system', ['sequences']); + + } + + /** + * Tests history_attach_timestamp() deprecation. + * + * @expectedDeprecation history_attach_timestamp() is deprecated in Drupal 8.8.0 and will be removed before Drupal 9.0.0. Use \Drupal\history\HistoryRenderCallback::lazyBuilder() instead. See https://www.drupal.org/node/2966725 + */ + public function testHistoryAttachTimestamp() { + $node = Node::create([ + 'title' => 'n1', + 'type' => 'default', + ]); + $node->save(); + + $user1 = User::create([ + 'name' => 'user1', + 'mail' => 'user1@example.com', + ]); + $user1->save(); + + \Drupal::currentUser()->setAccount($user1); + history_write(1); + + $render = history_attach_timestamp(1); + $this->assertEquals(REQUEST_TIME, $render['#attached']['drupalSettings']['history']['lastReadTimestamps'][1]); + } + +} diff --git a/core/modules/node/src/NodeViewBuilder.php b/core/modules/node/src/NodeViewBuilder.php index a3a5b7b1604..f36cd4863a4 100644 --- a/core/modules/node/src/NodeViewBuilder.php +++ b/core/modules/node/src/NodeViewBuilder.php @@ -4,11 +4,13 @@ namespace Drupal\node; use Drupal\Core\Entity\EntityInterface; use Drupal\Core\Entity\EntityViewBuilder; +use Drupal\Core\Render\Element\Link; +use Drupal\Core\Security\TrustedCallbackInterface; /** * View builder handler for nodes. */ -class NodeViewBuilder extends EntityViewBuilder { +class NodeViewBuilder extends EntityViewBuilder implements TrustedCallbackInterface { /** * {@inheritdoc} @@ -87,7 +89,7 @@ class NodeViewBuilder extends EntityViewBuilder { public static function renderLinks($node_entity_id, $view_mode, $langcode, $is_in_preview, $revision_id = NULL) { $links = [ '#theme' => 'links__node', - '#pre_render' => ['drupal_pre_render_links'], + '#pre_render' => [[Link::class, 'preRenderLinks']], '#attributes' => ['class' => ['links', 'inline']], ]; @@ -146,4 +148,13 @@ class NodeViewBuilder extends EntityViewBuilder { ]; } + /** + * {@inheritdoc} + */ + public static function trustedCallbacks() { + $callbacks = parent::trustedCallbacks(); + $callbacks[] = 'renderLinks'; + return $callbacks; + } + } diff --git a/core/modules/node/src/Plugin/Search/NodeSearch.php b/core/modules/node/src/Plugin/Search/NodeSearch.php index 955d073c467..777d2b8b7a4 100644 --- a/core/modules/node/src/Plugin/Search/NodeSearch.php +++ b/core/modules/node/src/Plugin/Search/NodeSearch.php @@ -15,6 +15,7 @@ use Drupal\Core\Form\FormStateInterface; use Drupal\Core\Language\LanguageInterface; use Drupal\Core\Language\LanguageManagerInterface; use Drupal\Core\Messenger\MessengerInterface; +use Drupal\Core\Security\TrustedCallbackInterface; use Drupal\Core\Session\AccountInterface; use Drupal\Core\Access\AccessibleInterface; use Drupal\Core\Database\Query\Condition; @@ -33,7 +34,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface; * title = @Translation("Content") * ) */ -class NodeSearch extends ConfigurableSearchPluginBase implements AccessibleInterface, SearchIndexingInterface { +class NodeSearch extends ConfigurableSearchPluginBase implements AccessibleInterface, SearchIndexingInterface, TrustedCallbackInterface { use DeprecatedServicePropertyTrait; /** @@ -850,4 +851,11 @@ class NodeSearch extends ConfigurableSearchPluginBase implements AccessibleInter } } + /** + * {@inheritdoc} + */ + public static function trustedCallbacks() { + return ['removeSubmittedInfo']; + } + } diff --git a/core/modules/system/tests/modules/form_test/src/Form/FormTestInputForgeryForm.php b/core/modules/system/tests/modules/form_test/src/Form/FormTestInputForgeryForm.php index 67d71b361e1..b581c9e488b 100644 --- a/core/modules/system/tests/modules/form_test/src/Form/FormTestInputForgeryForm.php +++ b/core/modules/system/tests/modules/form_test/src/Form/FormTestInputForgeryForm.php @@ -4,6 +4,7 @@ namespace Drupal\form_test\Form; use Drupal\Core\Form\FormBase; use Drupal\Core\Form\FormStateInterface; +use Drupal\Core\Security\TrustedCallbackInterface; use Symfony\Component\HttpFoundation\JsonResponse; /** @@ -11,7 +12,7 @@ use Symfony\Component\HttpFoundation\JsonResponse; * * @internal */ -class FormTestInputForgeryForm extends FormBase { +class FormTestInputForgeryForm extends FormBase implements TrustedCallbackInterface { /** * {@inheritdoc} @@ -68,4 +69,11 @@ class FormTestInputForgeryForm extends FormBase { return new JsonResponse($form_state->getValues()); } + /** + * {@inheritdoc} + */ + public static function trustedCallbacks() { + return ['postRender']; + } + } diff --git a/core/modules/system/tests/modules/pager_test/src/Controller/PagerTestController.php b/core/modules/system/tests/modules/pager_test/src/Controller/PagerTestController.php index 4e01479f38f..5048e131bd4 100644 --- a/core/modules/system/tests/modules/pager_test/src/Controller/PagerTestController.php +++ b/core/modules/system/tests/modules/pager_test/src/Controller/PagerTestController.php @@ -5,11 +5,12 @@ namespace Drupal\pager_test\Controller; use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Database\Database; use Drupal\Core\Database\Query\PagerSelectExtender; +use Drupal\Core\Security\TrustedCallbackInterface; /** * Controller routine for testing the pager. */ -class PagerTestController extends ControllerBase { +class PagerTestController extends ControllerBase implements TrustedCallbackInterface { /** * Builds a render array for a pageable test table. @@ -124,4 +125,11 @@ class PagerTestController extends ControllerBase { return $pager; } + /** + * {@inheritdoc} + */ + public static function trustedCallbacks() { + return ['showPagerCacheContext']; + } + } diff --git a/core/modules/system/tests/modules/render_placeholder_message_test/src/RenderPlaceholderMessageTestController.php b/core/modules/system/tests/modules/render_placeholder_message_test/src/RenderPlaceholderMessageTestController.php index ddb76e6dc05..d59ed4eddaf 100644 --- a/core/modules/system/tests/modules/render_placeholder_message_test/src/RenderPlaceholderMessageTestController.php +++ b/core/modules/system/tests/modules/render_placeholder_message_test/src/RenderPlaceholderMessageTestController.php @@ -2,11 +2,12 @@ namespace Drupal\render_placeholder_message_test; +use Drupal\Core\Security\TrustedCallbackInterface; use Drupal\Core\Render\RenderContext; use Symfony\Component\DependencyInjection\ContainerAwareInterface; use Symfony\Component\DependencyInjection\ContainerAwareTrait; -class RenderPlaceholderMessageTestController implements ContainerAwareInterface { +class RenderPlaceholderMessageTestController implements ContainerAwareInterface, TrustedCallbackInterface { use ContainerAwareTrait; @@ -97,4 +98,11 @@ class RenderPlaceholderMessageTestController implements ContainerAwareInterface return ['#markup' => '

Message: ' . $message . '

']; } + /** + * {@inheritdoc} + */ + public static function trustedCallbacks() { + return ['setAndLogMessage']; + } + } diff --git a/core/modules/system/tests/modules/system_test/src/Controller/SystemTestController.php b/core/modules/system/tests/modules/system_test/src/Controller/SystemTestController.php index cfd0f2f7967..2f86b8981fd 100644 --- a/core/modules/system/tests/modules/system_test/src/Controller/SystemTestController.php +++ b/core/modules/system/tests/modules/system_test/src/Controller/SystemTestController.php @@ -6,6 +6,7 @@ use Drupal\Core\Access\AccessResult; use Drupal\Core\Cache\CacheableResponse; use Drupal\Core\Controller\ControllerBase; use Drupal\Core\Messenger\MessengerInterface; +use Drupal\Core\Security\TrustedCallbackInterface; use Drupal\Core\Render\RendererInterface; use Drupal\Core\Render\Markup; use Drupal\Core\Session\AccountInterface; @@ -19,7 +20,7 @@ use Symfony\Component\DependencyInjection\ContainerInterface; /** * Controller routines for system_test routes. */ -class SystemTestController extends ControllerBase { +class SystemTestController extends ControllerBase implements TrustedCallbackInterface { /** * The lock service. @@ -398,4 +399,11 @@ class SystemTestController extends ControllerBase { return new CacheableResponse('Foo', 200, ['Cache-Control' => 'bar']); } + /** + * {@inheritdoc} + */ + public static function trustedCallbacks() { + return ['preRenderCacheTags']; + } + } diff --git a/core/modules/system/tests/src/Kernel/Theme/FunctionsTest.php b/core/modules/system/tests/src/Kernel/Theme/FunctionsTest.php index 9b93dff3ff8..2826a3225e6 100644 --- a/core/modules/system/tests/src/Kernel/Theme/FunctionsTest.php +++ b/core/modules/system/tests/src/Kernel/Theme/FunctionsTest.php @@ -5,6 +5,7 @@ namespace Drupal\Tests\system\Kernel\Theme; use Drupal\Component\Serialization\Json; use Drupal\Component\Utility\Html; use Drupal\Component\Render\FormattableMarkup; +use Drupal\Core\Render\Element\Link; use Drupal\Core\Session\UserSession; use Drupal\Core\Url; use Drupal\KernelTests\KernelTestBase; @@ -417,9 +418,114 @@ class FunctionsTest extends KernelTestBase { } /** - * Test the use of drupal_pre_render_links() on a nested array of links. + * Test the use of Link::preRenderLinks() on a nested array of links. + * + * @see \Drupal\Core\Render\Element\Link::preRenderLinks() */ public function testDrupalPreRenderLinks() { + // Define the base array to be rendered, containing a variety of different + // kinds of links. + $base_array = [ + '#theme' => 'links', + '#pre_render' => [[Link::class, 'preRenderLinks']], + '#links' => [ + 'parent_link' => [ + 'title' => 'Parent link original', + 'url' => Url::fromRoute('router_test.1'), + ], + ], + 'first_child' => [ + '#theme' => 'links', + '#links' => [ + // This should be rendered if 'first_child' is rendered separately, + // but ignored if the parent is being rendered (since it duplicates + // one of the parent's links). + 'parent_link' => [ + 'title' => 'Parent link copy', + 'url' => Url::fromRoute('router_test.6'), + ], + // This should always be rendered. + 'first_child_link' => [ + 'title' => 'First child link', + 'url' => Url::fromRoute('router_test.7'), + ], + ], + ], + // This should always be rendered as part of the parent. + 'second_child' => [ + '#theme' => 'links', + '#links' => [ + 'second_child_link' => [ + 'title' => 'Second child link', + 'url' => Url::fromRoute('router_test.8'), + ], + ], + ], + // This should never be rendered, since the user does not have access to + // it. + 'third_child' => [ + '#theme' => 'links', + '#links' => [ + 'third_child_link' => [ + 'title' => 'Third child link', + 'url' => Url::fromRoute('router_test.9'), + ], + ], + '#access' => FALSE, + ], + ]; + + // Start with a fresh copy of the base array, and try rendering the entire + // thing. We expect a single