Change record status: 
Project: 
Introduced in branch: 
8.8.x
Introduced in version: 
8.8.0
Description: 

Recent security releases have shown that the render system needs to be stricter about what it allow to be called by a callback. See:

If you have code that adds a render callback - #access_callback, #lazy_builder, #pre_render or #post_render it might need to be updated. This will initially result in deprecation errors, but will eventually result in code throwing an exception.

Procedural function callbacks

If the callback is to a procedural function, for example, color_block_view_pre_render() you will need to move it to an object that implements \Drupal\Core\Security\TrustedCallbackInterface. For example:

class ColorBlock implements TrustedCallbackInterface {

  /**
   * {@inheritdoc}
   */
  public static function trustedCallbacks() {
    return ['preRender'];
  }

  /**
   * #pre_render callback: Sets color preset logo.
   */
  public static function preRender($build) {
    // Do what the color_block_view_pre_render() function did. For the minimal change you can call the procedural function.
  }

}

In core:

  • drupal_pre_render_links() replaced with \Drupal\Core\Render\Element\Link::preRenderLinks()
  • color_block_view_pre_render() replaced with \Drupal\color\ColorBlock::preRender()
  • filter_form_access_denied() replaced with \Drupal\filter\Element\TextFormat::accessDeniedCallback()
  • history_attach_timestamp() replaced with \Drupal\history\HistoryRenderCallback::lazyBuilder()
  • _toolbar_do_get_rendered_subtrees() replaced with \Drupal\toolbar\Controller\ToolbarController::preRenderGetRenderedSubtrees()
  • toolbar_prerender_toolbar_administration_tray() replaced with \Drupal\toolbar\Controller\ToolbarController::preRenderAdministrationTray()
  • views_pre_render_views_form_views_form() replaced with \Drupal\views\Form\ViewsFormMainForm::preRenderViewsForm()

Object method callbacks

If the callback is a RenderCallbackInterface object no update is necessary. Otherwise the object needs to implement \Drupal\Core\Security\TrustedCallbackInterface. For example, \Drupal\node\Plugin\Search\NodeSearch has #pre_render callback to ::removeSubmittedInfo(). You need to add TrustedCallbackInterface to NodeSearch and implement TrustedCallbackInterface::trustedCallbacks() like so:

  /**
   * {@inheritdoc}
   */
  public static function trustedCallbacks() {
    return ['removeSubmittedInfo'];
  }

Anonymous function callbacks

These are not recommended because they cannot be serialized. But they do not require updating as they are trusted.

Impacts: 
Module developers
Themers

Comments

pingwin4eg’s picture

Here's an example of what could happen with an anonymous function as a #pre_render callback:

Exception: Serialization of 'Closure' is not allowed in serialize() (line 245 of core/lib/Drupal/Core/Cache/DatabaseBackend.php).

Drupal\Core\Cache\DatabaseBackend->doSetMultiple(Array) (Line: 193)
Drupal\Core\Cache\DatabaseBackend->setMultiple(Array) (Line: 181)
Drupal\Core\Cache\DatabaseBackend->set('element_info_build:styleswitcher_test_theme', Array, -1, Array) (Line: 180)
Drupal\Core\Cache\ChainedFastBackend->set('element_info_build:styleswitcher_test_theme', Array, -1, Array) (Line: 130)
Drupal\Core\Render\ElementInfoManager->buildInfo('styleswitcher_test_theme') (Line: 77)
Drupal\Core\Render\ElementInfoManager->getInfo('html') (Line: 300)
Drupal\Core\Render\Renderer->doRender(Array, ) (Line: 200)
Drupal\Core\Render\Renderer->render(Array) (Line: 147)
Drupal\Core\Render\MainContent\HtmlRenderer->Drupal\Core\Render\MainContent\{closure}() (Line: 573)
Drupal\Core\Render\Renderer->executeInRenderContext(Object, Object) (Line: 148)
Drupal\Core\Render\MainContent\HtmlRenderer->renderResponse(Array, Object, Object) (Line: 90)
Drupal\Core\EventSubscriber\MainContentViewSubscriber->onViewRenderArray(Object, 'kernel.view', Object) (Line: 78)
Symfony\Component\EventDispatcher\LegacyEventDispatcherProxy->dispatch(Object, 'kernel.view') (Line: 163)
Symfony\Component\HttpKernel\HttpKernel->handleRaw(Object, 1) (Line: 80)
Symfony\Component\HttpKernel\HttpKernel->handle(Object, 1, 1) (Line: 57)
Drupal\Core\StackMiddleware\Session->handle(Object, 1, 1) (Line: 47)
Drupal\Core\StackMiddleware\KernelPreHandle->handle(Object, 1, 1) (Line: 191)
Drupal\page_cache\StackMiddleware\PageCache->fetch(Object, 1, 1) (Line: 128)
Drupal\page_cache\StackMiddleware\PageCache->lookup(Object, 1, 1) (Line: 82)
Drupal\page_cache\StackMiddleware\PageCache->handle(Object, 1, 1) (Line: 47)
Drupal\Core\StackMiddleware\ReverseProxyMiddleware->handle(Object, 1, 1) (Line: 52)
Drupal\Core\StackMiddleware\NegotiationMiddleware->handle(Object, 1, 1) (Line: 23)
Stack\StackedHttpKernel->handle(Object, 1, 1) (Line: 705)
Drupal\Core\DrupalKernel->handle(Object) (Line: 19)
arafalov’s picture

I cannot seem to find any mentions or implementations of RenderElementInterface. Was RenderCallbackInterface by chance the one that was meant instead?

johnalbin’s picture

Good catch, Alexandre! I've updated the change record to use the correct interface.

  - John (JohnAlbin)

tinamrak’s picture

I'm trying to alter the commerce cart block with a custom module. I found an example here but this doesn't work with D9: https://www.drupal.org/project/commerce/issues/2880926.

I get this error:
Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function.

Here is my code:

function olla_common_block_view_commerce_cart_alter(array &$build, Drupal\Core\Block\BlockPluginInterface $block) {
  $build['#pre_render'][] = '_change_cart_build_content';
}
function _change_cart_build_content(array $build) {
  $count = $build['content']['#count'];
  $build['content']['#count_text'] = \Drupal::translation()->formatPlural($count, '(@count)', '(@count)');
  return $build;
}

I'm not sure how to fix this, can someone please help me?

rohitrajputsahab’s picture

Step 1:- Create a folder "src" under your custom module and add a file like "OllaBlockViewBuilder.php"
Step 2:- In the "OllaBlockViewBuilder.php", Please add the below code:-

<?php

namespace Drupal\olla_common;

use Drupal\Core\Render\Element\RenderCallbackInterface;

/**
 * Provides a trusted callback to alter the commerce cart block.
 *
 * @see olla_common_block_view_commerce_cart_alter()
 */
class OllaBlockViewBuilder implements RenderCallbackInterface {

  /**
   * Sets Olla common - #pre_render callback.
   */
  public static function preRender($build) {
    $count = $build['content']['#count'];
    $build['content']['#count_text'] = \Drupal::translation()->formatPlural($count, '(@count)', '(@count)');
    return $build;
  }

}

Step 3:- In your "olla_common.module" file. Please add

use Drupal\olla_common\OllaBlockViewBuilder;

AND

function olla_common_block_view_commerce_cart_alter(array &$build, Drupal\Core\Block\BlockPluginInterface $block) {
  $build['#pre_render'][] = [OllaBlockViewBuilder::class, 'preRender'];
}
imclean’s picture

Here's an example using TrustedCallbackInterface for step 2 below.


namespace Drupal\olla_common;

use Drupal\Core\Security\TrustedCallbackInterface;

/**
 * Provides a trusted callback to alter the commerce cart block.
 *
 * @see olla_common_block_view_commerce_cart_alter()
 */
class OllaBlockViewBuilder implements TrustedCallbackInterface {

   /**
    * {@inheritdoc}
    */
   public static function trustedCallbacks() {
    return ['preRender'];    
  }

  /**
   * Sets Olla common - #pre_render callback.
   */
  public static function preRender($build) {
    $count = $build['content']['#count'];
    $build['content']['#count_text'] = \Drupal::translation()->formatPlural($count, '(@count)', '(@count)');
    return $build;
  }

}
usmanjutt84’s picture

I am following this but throwing this error

TypeError: Argument 1 passed to Drupal\Core\Render\Renderer::doTrustedCallback() must be callable, array given, called in /app/web/core/lib/Drupal/Core/Render/Renderer.php on line 772 in Drupal\Core\Render\Renderer->doTrustedCallback() (line 51 of core/lib/Drupal/Core/Security/DoTrustedCallbackTrait.php).
Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 772)
Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 363)

I think issue is already created see https://www.drupal.org/project/drupal/issues/3224723

usmanjutt84’s picture

Well, it looks strange to me its working now after updating new controller namespace with the suffix as "Controller" like

namespace Drupal\olla_common\Controller;

nick hope’s picture

I have a custom module that adds an extra field to breadcrumbs on taxonomy terms from one vocabulary. It was working in D9.1 but since upgrading to D9.2 it is throwing a similar exception to tinamrak:

Error: Call to a member function loadAllParents() on null in Drupal\breadcrumb_extra_field\Service\BreadcrumbExtraFieldBreadcrumbBuilder->build() (line 34 of modules\custom\breadcrumb_extra_field\src\Service\BreadcrumbExtraFieldBreadcrumbBuilder.php)...
...Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function.

But it is a more complex case. Below is my src\Service\BreadcrumbExtraFieldBreadcrumbBuilder.php file. Line 34 is:
$parents = $this->termStorage->loadAllParents($term->id());

Can anyone please advise how I can fix this? Even a hint would be much appreciated.

(I only just got this module working in D9.1. The forum thread about it is here.)

<?php

namespace Drupal\breadcrumb_extra_field\Service;

use Drupal\Core\Link;
use Drupal\Core\Breadcrumb\Breadcrumb;
use Drupal\Core\Breadcrumb\BreadcrumbBuilderInterface;
use Drupal\Core\Routing\RouteMatchInterface;
use Drupal\taxonomy\TermBreadcrumbBuilder;
use Drupal\taxonomy\TermInterface;

/**
 * My custom breadcrumb builder for the living_things taxonomy.
 */
class BreadcrumbExtraFieldBreadcrumbBuilder extends TermBreadcrumbBuilder implements BreadcrumbBuilderInterface {

    /**
     * {@inheritdoc}
     */
    public function applies(RouteMatchInterface $route_match) {
        return $route_match->getRouteName() == 'entity.taxonomy_term.canonical'
            && ($term = $route_match->getParameter('taxonomy_term')) instanceof TermInterface
            && $term->bundle() == 'living_things';
    }

    /**
     * {@inheritdoc}
     */
    public function build(RouteMatchInterface $route_match) {
        $breadcrumb = new Breadcrumb();
        $breadcrumb->addLink(Link::createFromRoute($this->t('Home'), ''));
        $term = $route_match->getParameter('taxonomy_term');
        $breadcrumb->addCacheableDependency($term);
        $parents = $this->termStorage->loadAllParents($term->id());
        array_shift($parents);

        foreach (array_reverse($parents) as $term) {
            $term = \Drupal::service('entity.repository')->getTranslationFromContext($term);
            $my_field = $term->get('field_most_common_name');
            $term_name = $term->getName() . (!empty($my_field->getValue()[0]['value']) ? ' (' . $my_field->getValue()[0]['value'] . ')' : '');

            $breadcrumb->addCacheableDependency($term);
            $breadcrumb->addLink(Link::createFromRoute($term_name, 'entity.taxonomy_term.canonical', ['taxonomy_term' => $term->id()]));
        }

        $breadcrumb->addCacheContexts(['route']);
        return $breadcrumb;
    }
}
hassebasse’s picture

Maybe my experience can give anyone some input.

I relized the problem in 9.2.4, but after reinstalling older backups I can see the problem was there already in 9.1.x.

The error came when trying to reach the settings in the module background_image, and the error was:

Drupal\Core\Security\UntrustedCallbackException : Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was Drupal\background_image\Form\BackgroundImageForm::preRenderStates. See https://www.drupal.org/node/2966725 dans Drupal\Core\Render\Renderer->doTrustedCallback() (ligne 96 de /home/domaine.com/public_html/project/web/core/lib/Drupal/Core/Security/DoTrustedCallbackTrait.php).

The culprit was a Core patch used for the Core module Forums. here is the origin of that issue: https://www.drupal.org/project/drupal/issues/2010132

lapurddrupal’s picture

Version 9.3.14 PHP 8.0 I got the the Error:
The website encountered an unexpected error. Please try again later.
TypeError: strpos(): Argument #1 ($haystack) must be of type string, array given in strpos() (line 151 of core/lib/Drupal/Core/Utility/LinkGenerator.php).

strpos(Array, '<') (Line: 151)
Drupal\Core\Utility\LinkGenerator->generate('en', Object) (Line: 95)
Drupal\Core\Render\Element\Link::preRenderLink(Array)
call_user_func_array(Array, Array) (Line: 101)
Drupal\Core\Render\Renderer->doTrustedCallback(Array, Array, 'Render #pre_render callbacks must be methods of a class that implements \Drupal\Core\Security\TrustedCallbackInterface or be an anonymous function. The callback was %s. See https://www.drupal.org/node/2966725', 'exception', 'Drupal\Core\Render\Element\RenderCallbackInterface') (Line: 772)
Drupal\Core\Render\Renderer->doCallback('#pre_render', Array, Array) (Line: 363)