Problem/Motivation

Local action plugins do not have any way to provide cacheability metadata.

I came to this realization when trying to port a contrib module where the local action link will change based on the module's configuration setting. It's easy to write a local action plugin that varies the route and/or route parameters based on a config setting, but the render cache will continue to hold the wrong version.

Since the D7 version of the module selectively populates a CSRF token in the rendered link depending on the config setting (hence not cached at all in the render cache), this incorrect caching would be critical problem.

This is because the local action plugin manager has no way to accumulate cacheability metadata and we make no provision for local action plugins that implement the CacheableDependencyInterface

This was addressed for local tasks in #2511516: Make local tasks and actions hooks provide cacheability metadata, to make the blocks showing them cacheable but accidentally omitted for local actions.

Proposed resolution

Modify the local action manager to be in line with the local task manager (though this might be an interface change), or see if it's possible to change the internals of the implementation so cachability metadata is added to the render array.

Remaining tasks

write patch, add tests

User interface changes

none

API changes

TBD

Data model changes

none

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

pwolanin created an issue. See original summary.

pwolanin’s picture

Status: Active » Needs review
FileSize
1.1 KB

Possibly we could do something like this? Need to add some tests to verify the behavior.

Need feedback from Wim Leers and laurii or someone else who understands this cacheable metadata system.

pwolanin’s picture

Or maybe like this? Since we want to bubble up any cachabilty data from the access check also?

Status: Needs review » Needs work

The last submitted patch, 3: 2624594-3.patch, failed testing.

pwolanin’s picture

Status: Needs work » Needs review
FileSize
5.98 KB

Ok, need to add the #cache elements to the unit test data.

dawehner’s picture

Note: We have the same issue with contextual links ...

lauriii’s picture

Assigned: Unassigned » lauriii

Working on this one for a bit

lauriii’s picture

One of the problems I can figure out right now is that links depend on the route cache context, but its only set on the block. There probably should be additional parameter ::getActionsForRoute that sets the cacheability metadata for the whole link set.

pwolanin’s picture

@laurii - to avoid changing the interface, we could have LocalActionManager::getActionsForRoute set #cache on the top level of the render array, but I'm a little unclear on some of the details of the render caching in terms where these properties are set.

@dawehner - indeed, contextual links seem to suffer the same, but it's possibly harder to solve since we are not returning a render array there.

I'm also not sure (in terms of BC) if we should assume links that don't implement CacheableDependencyInterface should have max-age of 0 or forever. I guess 0 is the more "conservative" approach in terms of not over-caching, even though if there are any contrib modules with custom plugin classes they'd need to be updated.

dawehner’s picture

I'm also not sure (in terms of BC) if we should assume links that don't implement CacheableDependencyInterface should have max-age of 0 or forever. I guess 0 is the more "conservative" approach in terms of not over-caching, even though if there are any contrib modules with custom plugin classes they'd need to be updated.

What do we do in other places in core? Views for example doesn't deal with it, see \Drupal\views\Plugin\views\display\DisplayPluginBase::calculateCacheMetadata

pwolanin’s picture

@dawehner, for local tasks, if they don't implement that interface they get a max-age of 0, so doing the same in other places would be consistent at least.

pwolanin’s picture

Adding route cache context to the render array per laurii. Not sure if the block still needs to have it also?

lauriii’s picture

Status: Needs review » Needs work
Issue tags: +Needs tests

So right now we have this in the build method of the block:

    if (empty($local_actions)) {
      return [];
    }

and that will remove the cache context away from the render array. That logic has to change so that #cache item wont get removed and then we can remove the ::getCacheContexts from the block.

After we've remove the method we should try to investigate whether there is already existing test coverage for this, or if we should add new tests.

lauriii’s picture

Assigned: lauriii » Unassigned

Not working on the issue right now so unassigning

dawehner’s picture

Priority: Critical » Major

Local actions are used on admin pages, which aren't cached. I don't think this is a critical issue but rather a major one.

pwolanin’s picture

Status: Needs work » Needs review
FileSize
7.76 KB
779 bytes

ok, fixing the block code, but still need tests.

Wim Leers’s picture

Local actions are used on admin pages, which aren't cached. I don't think this is a critical issue but rather a major one.

+1

pwolanin’s picture

In contrib, they are added to node view pages, etc. So this is pretty serious - it's not admin only

pwolanin’s picture

Here's first pass at a test that seems to validate the expected behavior

The test-only patch is also the interdiff.

pwolanin’s picture

Some small textual fixes.

The last submitted patch, 19: 2624594-19-test-only.patch, failed testing.

lauriii’s picture

Status: Needs review » Reviewed & tested by the community

Finally got a chance to review the tests and the patch. Looks good for me!

alexpott’s picture

Status: Reviewed & tested by the community » Needs review
+++ b/core/tests/Drupal/Tests/Core/Menu/LocalActionManagerTest.php
@@ -206,6 +209,11 @@ public function getActionsForRouteProvider() {
+            'max-age' => 0,

@@ -240,6 +251,11 @@ public function getActionsForRouteProvider() {
+            'max-age' => 0,

@@ -275,6 +294,11 @@ public function getActionsForRouteProvider() {
+            'max-age' => 0,

@@ -285,6 +309,11 @@ public function getActionsForRouteProvider() {
+            'max-age' => 0,

@@ -322,6 +354,11 @@ public function getActionsForRouteProvider() {
+            'max-age' => 0,

@@ -332,6 +369,11 @@ public function getActionsForRouteProvider() {
+            'max-age' => 0,

Will this change result in things being uncachable that once were?

pwolanin’s picture

Yes, any e.g. custom contrib local action classes that are not extending LocaActionDefault will be uncached until they add cacheability metadata.

LocalAction Default returns: Cache::PERMANENT; so in general there will be no change from current behavior.

lauriii’s picture

Status: Needs review » Reviewed & tested by the community

I think this is still RTBC and ready for the commiter feedback again

Wim Leers’s picture

Status: Reviewed & tested by the community » Needs work
+++ b/core/modules/system/src/Tests/Menu/LocalActionTest.php
@@ -48,6 +48,22 @@ public function testLocalAction() {
+    // Test a local action title that changes based on a config value.

+++ b/core/modules/system/tests/modules/menu_test/menu_test.links.action.yml
@@ -11,6 +11,14 @@ menu_test.local_action5:
+  cache_tags:
+    - config:menu_test.links.action

But this is not actually tested AFAICT, because of the default of max-age = 0.

The menu_test.local_action.cache_check local action should specify cache_max_age: 1000000 or something like that.

Or what am I missing?

pwolanin’s picture

@Wim Leers - I don't understand your comment. The default is only 0 if the plugin doesn't implement CacheableDependencyInterface.
Since TestLocalActionWithConfig extends LocalActionDefault it will have a max-age of Cache::PERMANENT.

Is there another test case you'd want to see added?

Xano’s picture

Xano’s picture

Is there a way we can prevent adding more array items, and use interfaced objects for this, just like elsewhere in core? #2633878: Finalize cacheability for plugins solves a different problem by also adding cacheability metadata to plugin definitions, but in the form of CacheableDependencyInterface like we use elsewhere in core. Since we should convert plugin definitions to objects in the future anyway, this seems like a good place to start. The managers could convert YAML arrays to objects in this case.

Xano’s picture

After talking to @pwolanin on IRC I now understand the exact reason for these additional keys. It would not make sense to use a generic cache key just like in the other definition. Maybe we can use render_cache_* to emphasize those values are for render array cacheability only?

Wim Leers’s picture

Status: Needs work » Reviewed & tested by the community

I don't understand your comment. The default is only 0 if the plugin doesn't implement CacheableDependencyInterface.

The answer to this is:

Or what am I missing?

… clearly I was missing something :) I misread. Sorry.

Xano’s picture

Since we should allow plugin definitions to expose their own cacheability metadata (in another issue), can we prefix the definition keys here so it's absolutely clear those influence render array cacheability and not something else?

pwolanin’s picture

@Xano - based on our discussion in IRC, possibly we should not put the cache info in the YAML at all, but rather require it to be defined in the PHP?

The implementation here just mirrors what is already in core for local tasks. We should probably make them work the same way, and I'm guessing changing local tasks now would be considered a BC break. That's the best argument, I think, for keeping the patch as-is. If you can convince xjm that changing the YAML keys or the whole scheme for local tasks makes sense, we can do it.

Xano’s picture

@Xano - based on our discussion in IRC, possibly we should not put the cache info in the YAML at all, but rather require it to be defined in the PHP?

I don't think we should in this case, because it would undo the benefits of YAML discovery (not having to write your own class). It's more suitable when used with annotated class discovery.

I checked the code again, and see that LocalTaskDefault does the same thing already. We should probably use the same approach for LocalActionDefault and not prefix the definition keys for consistency with existing code.

pwolanin’s picture

@Xano - yes, I think at this point consistency is the easiest way forward, but it will rarely be the case that you need some non-default redner cache metadata unless you are also implementing a custom class.

alexpott’s picture

Version: 8.0.x-dev » 8.1.x-dev
Status: Reviewed & tested by the community » Fixed

I'm committing this to 8.1.x first. I'm not sure this should go into 8.0.x as it is adding an interface and public methods to a class. Committed 1a71d3b and pushed to 8.1.x. Thanks!

diff --git a/core/lib/Drupal/Core/Menu/Plugin/Block/LocalActionsBlock.php b/core/lib/Drupal/Core/Menu/Plugin/Block/LocalActionsBlock.php
index 98427aa..eda4a87 100644
--- a/core/lib/Drupal/Core/Menu/Plugin/Block/LocalActionsBlock.php
+++ b/core/lib/Drupal/Core/Menu/Plugin/Block/LocalActionsBlock.php
@@ -8,7 +8,6 @@
 namespace Drupal\Core\Menu\Plugin\Block;
 
 use Drupal\Core\Block\BlockBase;
-use Drupal\Core\Cache\Cache;
 use Drupal\Core\Menu\LocalActionManagerInterface;
 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
 use Symfony\Component\DependencyInjection\ContainerInterface;
diff --git a/core/modules/system/src/Tests/Menu/LocalActionTest.php b/core/modules/system/src/Tests/Menu/LocalActionTest.php
index beb8870..c876a25 100644
--- a/core/modules/system/src/Tests/Menu/LocalActionTest.php
+++ b/core/modules/system/src/Tests/Menu/LocalActionTest.php
@@ -51,7 +51,7 @@ public function testLocalAction() {
     // Test a local action title that changes based on a config value.
     $this->drupalGet(Url::fromRoute('menu_test.local_action6'));
     $this->assertLocalAction([
-      [Url::fromRoute('menu_test.local_action5'), 'Original title']
+      [Url::fromRoute('menu_test.local_action5'), 'Original title'],
     ]);
     // Verify the expected cache tag in the response headers.
     $header_values = explode(' ', $this->drupalGetHeader('x-drupal-cache-tags'));
@@ -62,7 +62,7 @@ public function testLocalAction() {
     $config->save();
     $this->drupalGet(Url::fromRoute('menu_test.local_action6'));
     $this->assertLocalAction([
-      [Url::fromRoute('menu_test.local_action5'), 'New title']
+      [Url::fromRoute('menu_test.local_action5'), 'New title'],
     ]);
   }

Fixed up a few coding standards on commit.

  • alexpott committed 1a71d3b on 8.1.x
    Issue #2624594 by pwolanin, lauriii, Xano: Local action plugins do not...
pwolanin’s picture

Version: 8.1.x-dev » 8.0.x-dev
Status: Fixed » Reviewed & tested by the community

@alexpott - I think this is really important to go to 8.0.x. This is essential for making contrib modules work correctly with the render cache.

Wim Leers’s picture

I think the disruption here is super minimal: a default plugin manager implementation now implements one additional interface. No interfaces are changed. So I don't see why this couldn't go into 8.0. Even existing subclasses of this plugin manager would continue to work just fine.

This is a straight bugfix, with no BC impact, so can be safely committed to 8.0.x.

pwolanin’s picture

From discussion with xjm and alexpott in IRC - will try to re-roll the patch for 8.0.x without the changes to the default class, but with enough changes to the manager and block that a custom plugin class could bubble up cache tags.

Wim Leers’s picture

Status: Reviewed & tested by the community » Needs work

Adjusting status for #40.

pwolanin’s picture

Status: Needs work » Needs review
FileSize
13.17 KB
6.83 KB

Ok, new patch for 8.0.x that follows the plan from IRC as I understood it.

Status: Needs review » Needs work

The last submitted patch, 42: 2624594-42.patch, failed testing.

pwolanin’s picture

Status: Needs work » Needs review
FileSize
13.77 KB
2.46 KB

Needed to make a change to the unit test for the BC behavior.

Also - looking again at the fact we were not currently adding cacheability metadata for the access object, this fix does have some possible security implications for contrib.

dawehner’s picture

Could we provide a class in core which allows you to define cache metadata as part of the yml file, which itself extends from the default class

pwolanin’s picture

@dawehner - well, that's basically what's in 8.1.x? Or you are thinking as a shim for 8.0.x?

I think this is still enough of an edge case that it's not really needed. For example, the implementation in the test class here would work perfectly fine in 8.1.x, and in general defining the cacheability in the class code may be a better pattern than using the yaml file.

dawehner’s picture

@dawehner - well, that's basically what's in 8.1.x? Or you are thinking as a shim for 8.0.x?

Well yeah I was thinking about making it at least easy by providing that class, so people can specify it in the yml file.

pwolanin’s picture

@dawhener - I don't see a lot of value to such a helper given that adding cache tags or contexts is an uncommon need. But, I can certainly add one if alexpott wants it.

dawehner’s picture

I think it would be nice because it also tells people that they should be thinking about it.

dawehner’s picture

Status: Needs review » Reviewed & tested by the community

Yeah potentially this is not that much needed in reality.

alexpott’s picture

Status: Reviewed & tested by the community » Fixed

Thanks @pwolanin for working on a backportable version for 8.0.x. Committed 99158a9 and pushed to 8.0.x. Thanks!

  • alexpott committed 99158a9 on 8.0.x
    Issue #2624594 by pwolanin, lauriii, Xano: Local action plugins do not...

Status: Fixed » Closed (fixed)

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