Problem/Motivation

This is a Views-specific problem in HEAD. But it's a problem that any controller that is using render caching for the entire render array it returns can experience. Because:

class Controller {
  public function something() {
    return [
      '#cache' => ['keys' => […], …],
      '#title' => 'Llamas are awesome',
      'children' => […],
    ];
  } 
}

means that on a cache hit, #title will not be set.

Steps to reproduce

  1. Install standard, login and visit front page
  2. You see the page title "Welcome to site"
  3. Reload the page

Expected: You see the same title again.

Actual: No title.

Proposed resolution

When a controller returns a render array, and it has #cache[keys] set to enable render caching (i.e. the entire render array returned by the controller is render cacheable), then we need to make sure that it doesn't lose #title. Because we allow #title to be set on the page-level render array (remember $page['#title'] in D7), and render caching only caches #markup, #cache and #attached.

Remaining tasks

None.

User interface changes

None.

API changes

None.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

olli’s picture

Title: Welcome to site dissappears after reload » Welcome to site disappears after reload
Issue summary: View changes
Issue tags: +D8 cacheability
FileSize
21.7 KB

Changing cache setting to 'none' solves the problem...

dawehner’s picture

Issue tags: +Needs tests

Oh I see, we probably need to add #title to #cache_properties by default on controller resuts?

catch’s picture

Priority: Normal » Major

This is quite disconcerting and might be a general issue (although entity titles are OK, but we do special things there anyway). Bumping to major.

olli’s picture

FileSize
1.35 KB

Taxonomy term pages have the same problem. Feed looks ok. For blocks these area and argument handler title overrides don't seem to work at all.

Here's a patch that adds '#title' to '#cache_properties' for page displays. Title does not disappear now but is double escaped on reload.

dawehner’s picture

Status: Active » Needs review
FileSize
2.84 KB
3.64 KB

I think the right fix is something like this.

+1 for merging test coverage together here ...

The last submitted patch, 5: 2505989-test.patch, failed testing.

olli’s picture

Component: views.module » base system

Oh I see, this is more general issue. For nodes, the title seems to work because after reload we get it from _title_callback. It does appear but quick edit is not working properly.

olli’s picture

Issue summary: View changes
Status: Needs review » Needs work
FileSize
28.67 KB

With #5, article title looks like this after reload:

dawehner’s picture

Status: Needs work » Needs review
Issue tags: -Needs tests
FileSize
3.29 KB
929 bytes

@olli

I tried hard to reproduce that, but I could not. Are you sure this is now still a problem?

We have tests now.

xjm’s picture

Priority: Major » Critical
Issue tags: +D8 Accelerate London

Uh. Yeah I'd consider this critical. Even though it's confined to one feature, it's a pretty important one.

Wim Leers’s picture

I'm pretty sure this is a Views-specific problem in HEAD. But it's a problem that any controller that is using render caching for the entire render array it returns can experience. Because:

class Controller {
  public function something() {
    return [
      '#cache' => ['keys' => […], …],
      '#title' => 'Llamas are awesome',
      'children' => […],
    ];
  } 
}

means that on a cache hit, #title will not be set.

Berdir’s picture

I noticed this in page manager a while ago, moved to a nested element as a workaround.

Status: Needs review » Needs work

The last submitted patch, 9: 2505989-9.patch, failed testing.

xjm’s picture

Hmm, is there any way we could add a general test for the scenario described in #11 and/or somehow flag render arrays like that?

Also I think we really need to document #11 thoroughly somewhere.

Wim Leers’s picture

#14: #9 is trying to fix it generically :)

dawehner’s picture

I'm pretty sure this is a Views-specific problem in HEAD. But it's a problem that any controller that is using render caching for the entire render array it returns can experience. Because:

Yeah just like #9 fixes it.

Working on a test.

chx’s picture

and a better title too. Thanks!

dawehner’s picture

Status: Needs work » Needs review
FileSize
3.11 KB
2.01 KB

Mh, I'm not sure its worth to unit test \Drupal\Core\Render\MainContent\HtmlRenderer

Regarding the general fix, I think its not possible. We want to get rid of pretty much everything beside #markup, so well, people need to use the #cache_properties api.

Wim Leers’s picture

Title: Welcome to site disappears after reload » Controllers render caching at the top level and setting a custom page title lose the title on render cache hits
dawehner’s picture

FileSize
3.75 KB
649 bytes

Thank you to alexpott for helping me here, well its still not fixed actually, mhh.

Wim Leers’s picture

Regarding the general fix, I think its not possible. We want to get rid of pretty much everything beside #markup, so well, people need to use the #cache_properties api.

Well, I'd argue that the changes in HtmlRenderer are the generic fix.

  1. +++ b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
    @@ -182,7 +182,10 @@ protected function prepare(array $main_content, Request $request, RouteMatchInte
           if (!empty($main_content)) {
    +
             $this->renderer->executeInRenderContext(new RenderContext(), function() use (&$main_content) {
    

    This is an unnecessary change.

  2. +++ b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
    @@ -182,7 +182,10 @@ protected function prepare(array $main_content, Request $request, RouteMatchInte
    +          // Ensure that we at least store the page title in cache.
    

    What about something like Retain #title; otherwise dynamically generated titles would be missing for controllers whose entire returned render array is render cached.

  3. +++ b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
    @@ -182,7 +182,10 @@ protected function prepare(array $main_content, Request $request, RouteMatchInte
    +          $main_content['#cache_properties'][] = '#title';
    

    We could wrap this in

    if (isset($main_content['#cache']['keys']) {
      …
    }
    

The last submitted patch, 18: 2505989-17.patch, failed testing.

Status: Needs review » Needs work

The last submitted patch, 20: 2505989-19.patch, failed testing.

larowlan’s picture

Assigned: Unassigned » larowlan

giddyup

larowlan’s picture

So the failing test is because the title retrieved from cache is sanitized again.
Working on it

dawehner’s picture

Assigned: larowlan » alexpott

This is a alex thing atm.

alexpott’s picture

Assigned: alexpott » Unassigned
Status: Needs work » Needs review
FileSize
7.04 KB
8.51 KB

So it turns out we need to know which # properties are safe when we render cache them.

Fabianx’s picture

+++ b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
@@ -182,7 +182,10 @@ protected function prepare(array $main_content, Request $request, RouteMatchInte
+          // Ensure that we at least store the page title in cache.
+          $main_content['#cache_properties'][] = '#title';

The problem is that the #cache_properties API only works with children to mark them as Safe again.

There is two cases here:

a) #title is already at top-level, in which case we could check if its safe and wrap in a SafeString instead.

b) #title is bubbled up dynamically, which is really hard.

( and #post_render does not work as call_user_func does not pass $elements by reference ), which in strict mode could be considered a regression compared to Drupal 7 ...

So not many options.

Probably the best is to do it like we do it with drupal_set_message() and check SafeMarkup::isSafe() for each string property and then either store as array with [ safe => TRUE ] case and unwrap array on retrieval or wrap directly in a SafeString object.

Wim Leers’s picture

Asked plach & Fabianx to review the changes in RenderCache, since they brought that to core.

Fabianx’s picture

+++ b/core/lib/Drupal/Core/Render/Renderer.php
@@ -257,16 +257,20 @@ protected function doRender(&$elements, $is_root_call = FALSE) {
+        // Mark root element cached properties as safe. This always includes
+        // #markup.
+        foreach ($elements['#safe_cache_properties'] as $cache_property) {
+          SafeMarkup::set($cached_element[$cache_property]);
+        }
+

Love it, lets clean this internal property up by unsetting it here.


RTBC once that change is made from me.

Status: Needs review » Needs work

The last submitted patch, 27: 2505989.27.patch, failed testing.

Wim Leers’s picture

  1. +++ b/core/lib/Drupal/Core/Render/MainContent/HtmlRenderer.php
    @@ -183,6 +183,8 @@ protected function prepare(array $main_content, Request $request, RouteMatchInte
             $this->renderer->executeInRenderContext(new RenderContext(), function() use (&$main_content) {
    +          // Ensure that we at least store the page title in cache.
    +          $main_content['#cache_properties'][] = '#title';
               return $this->renderer->render($main_content, FALSE);
             });
    

    See #21.2 & 3, I think that'd help make this significantly more understandable/less mysterious.

  2. +++ b/core/lib/Drupal/Core/Render/RenderCache.php
    @@ -335,6 +336,15 @@ public function getCacheableRenderArray(array $elements) {
    +      // Store whether any of the cache properties are safe strings. #markup is
    +      // always safe at this point.
    +      $data['#safe_cache_properties'] = ['#markup'];
    +      foreach (Element::properties(array_flip($elements['#cache_properties'])) as $cache_property) {
    +        if (isset($elements[$cache_property]) && SafeMarkup::isSafe($elements[$cache_property])) {
    +          $data['#safe_cache_properties'][] = $cache_property;
    +        }
    +      }
    

    Shouldn't this also require updates to the test coverage in \Drupal\Tests\Core\Render\RendererTest::testRenderCacheProperties()?

  3. +++ b/core/modules/system/tests/modules/test_page_test/src/Controller/Test.php
    @@ -6,6 +6,7 @@
     namespace Drupal\test_page_test\Controller;
    +use Drupal\Component\Utility\SafeMarkup;
    

    Nit: should have a \n in between.

After that, RTBC from me.

alexpott’s picture

Status: Needs work » Needs review
FileSize
5.38 KB
10.74 KB

Fixed #30 and #32.

Fingers-crossed for green-ness.

Status: Needs review » Needs work

The last submitted patch, 33: 2505989.33.patch, failed testing.

alexpott’s picture

Status: Needs work » Needs review
FileSize
5.32 KB
16.06 KB

Fixing the unit tests...

Status: Needs review » Needs work

The last submitted patch, 35: 2505989.35.patch, failed testing.

alexpott’s picture

Status: Needs work » Needs review

The fail in #35 was because this got a PHP5.5 bot without APCu.

alexpott queued 35: 2505989.35.patch for re-testing.

The last submitted patch, 4: 2505989-4.patch, failed testing.

Wim Leers’s picture

Issue summary: View changes
Status: Needs review » Reviewed & tested by the community
Issue tags: -Needs issue summary update

IS updated.

RTBC.

dawehner’s picture

Looks great from my point of view!

chx’s picture

Status: Reviewed & tested by the community » Needs work

Doxygen? RendererInterface::render mentions #cache_properties so I presume we would want to put #safe_cache_properties there as well.

Wim Leers’s picture

Good point.

alexpott’s picture

Status: Needs work » Needs review
FileSize
4.52 KB
18.76 KB

I’m concerned about the implementation.

At the moment if you cached the following render array

[
  ‘#title’ => ‘<em>I love emphasis</em>’,
  ‘#custom_property’ => [‘some’ => ‘random array’],
  ‘#markup’ => ‘<some html>’,
]

With the #custom_property it will break. Let’s make this more robust.

Also added docs to RenderCacheInterface - this doesn't belong along side #cache_properties in RendererInterface::render because it is never return from render as it is unset there. Perhaps we should move all of this SafeMarkup logic inside RenderCache::get()?

alexpott’s picture

FileSize
9.86 KB
16.53 KB

Yep we can deal with #safe_cache_properties internally in RenderCache.. I think the solution attached is much neater as #safe_cache_properties is never exposed to the render system at large.

Wim Leers’s picture

Status: Needs review » Reviewed & tested by the community

This is so much more elegant. Excellent :)

  1. +++ b/core/tests/Drupal/Tests/Core/Render/RendererTest.php
    @@ -690,10 +691,13 @@ public function testRenderCacheProperties(array $expected_results) {
    +      '#custom_property' => SafeMarkup::checkPlain('custom_value'),
    +      '#custom_property_array' => ['custom value'],
    

    Nit: I think it'd be clearer if these were named, #custom_property_safe and #custom_property_unsafe, respectively.

  2. +++ b/core/tests/Drupal/Tests/Core/Render/RendererTest.php
    @@ -708,9 +712,14 @@ public function testRenderCacheProperties(array $expected_results) {
    +    // #custom_property_array can not be a safe_cache_property.
    

    Though this makes it sufficiently clear. Consider the previous point a soft suggestion :)

Fabianx’s picture

  1. +++ b/core/lib/Drupal/Core/Render/RenderCache.php
    @@ -327,6 +333,7 @@ public function getCacheableRenderArray(array $elements) {
    +      '#safe_cache_properties' => []
    

    Do we need to expose this here?

  2. +++ b/core/lib/Drupal/Core/Render/RenderCache.php
    @@ -335,6 +342,13 @@ public function getCacheableRenderArray(array $elements) {
    +      // Store whether any of the cache properties are safe strings.
    +      foreach (Element::properties(array_flip($elements['#cache_properties'])) as $cache_property) {
    +        if (isset($elements[$cache_property]) && !is_array($elements[$cache_property]) && SafeMarkup::isSafe($elements[$cache_property])) {
    +          $data['#safe_cache_properties'][] = $cache_property;
    +        }
    +      }
    

    Should we not check for is_scalar() instead?

chx’s picture

I still do not see #safe_cache_properties documented...

alexpott’s picture

@chx I don't think it needs to be. It is set in RenderCache::set() and removed in RenderCache::get() - and also will be removed when we do #2506581: Remove SafeMarkup::set() from Renderer::doRender. It is never exposed in the render system.

olli’s picture

Status: Reviewed & tested by the community » Needs review

Manually tested the patch and it seems to fix the title for views (tested front page and term page). It also fixes quick editing of node titles on cache hit!

  1. +++ b/core/modules/node/src/Controller/NodeViewController.php
    @@ -57,7 +57,7 @@ public function view(EntityInterface $node, $view_mode = 'full', $langcode = NUL
       public function title(EntityInterface $node) {
    -    return SafeMarkup::checkPlain($this->entityManager->getTranslationFromContext($node)->label());
    +    return $this->entityManager->getTranslationFromContext($node)->label();
       }
    

    Why was this changed? With the patch applied, this method is not called anymore on cache hit.

  2. +++ b/core/modules/system/src/Tests/System/PageTitleTest.php
    @@ -137,6 +138,24 @@ public function testRoutingTitle() {
    +
    +    // Ensure that titles are cacheable and are escaped normally if the
    +    // controller does not escape them.
    +    $this->drupalGet('test-page-cached-controller');
    +    $this->assertTitle('Cached title | Drupal');
    +    $this->assertText(SafeMarkup::checkPlain('<span>Cached title</span>'));
    +    $this->drupalGet('test-page-cached-controller');
    +    $this->assertTitle('Cached title | Drupal');
    +    $this->assertText(SafeMarkup::checkPlain('<span>Cached title</span>'));
    +
    +    // Ensure that titles are cacheable and are escaped normally if the
    +    // controller escapes them use SafeMarkup::checkPlain().
    +    $this->drupalGet('test-page-cached-controller-safe');
    +    $this->assertTitle('Cached title | Drupal');
    +    $this->assertText(SafeMarkup::checkPlain('<span>Cached title</span>'));
    +    $this->drupalGet('test-page-cached-controller-safe');
    +    $this->assertTitle('Cached title | Drupal');
    +    $this->assertText(SafeMarkup::checkPlain('<span>Cached title</span>'));
    

    The assertTitle() in the second case should see <span> because that is escaped in '#title', right?

olli’s picture

Should we also add a case where title contains some html that is not escaped? Somethink like "Cached title with some html"

dawehner’s picture

Why was this changed? With the patch applied, this method is not called anymore on cache hit.

To be clear, this is exactly what auto escaping is doing for you. There is no longer a need to do that manually.

alexpott’s picture

re #51

Should we also add a case where title contains some html that is not escaped? Somethink like "Cached title with some html"

- well this is not possible with a Node - since it is alway checkPlain'd (before and after this patch) but I do think it is worth discussing was is and what is not allowed in a title. Just not in this issue.

re #50.2 - nope spans are removed for the title... See template_process_html

  // Construct page title.
  if (!empty($variables['page']['#title'])) {
    $head_title = array(
      'title' => trim(strip_tags($variables['page']['#title'])),
      'name' => $site_config->get('name'),
    );
  }

And now we're not checkPlain'ing this NodeViewController::title() this actually works as desired.

dawehner’s picture

Status: Needs review » Reviewed & tested by the community
Related issues: +#2530474: Discuss whether | which html tags to allow in entity labels

Let's do that in a separate issue: #2530474: Discuss whether | which html tags to allow in entity labels

I think with the response of alex, the feedback from olli got adressed

catch’s picture

Status: Reviewed & tested by the community » Fixed

Committed/pushed to 8.0.x, thanks!

  • catch committed 2378f93 on 8.0.x
    Issue #2505989 by alexpott, dawehner, olli: Controllers render caching...
olli’s picture

Thanks @dawehner and @alexpott for the replies and fixing this!

Found the problem in #50.2 and filed #2530908: Caching problem in PageTitleTest.

Fabianx’s picture

Hmm, #47 was not addressed :/.

So asking post-commit #47 again:

1. Why do we expose #safe_cache_properties in getCacheableRenderArray()? It should be an internal implementation detail only ...
2. Why not use is_scalar() instead of the is_array check? What happens with objects?

Both could be fixed in a non-critical follow-up, but would really love to have those answered.

Wim Leers’s picture

Why do we expose #safe_cache_properties in getCacheableRenderArray()? It should be an internal implementation detail only ...

Darn! I completely overlooked that :(

Let's open a major follow-up to fix the things we didn't get right here. Sorry for having missed that, Fabianx! :(

alexpott’s picture

Status: Fixed » Closed (fixed)

Automatically closed - issue fixed for 2 weeks with no activity.