Problem/Motivation
In \Drupal\entity_embed\Plugin\Filter\EntityEmbedFilter::process(), I see two problems:
-
$access_metadata = CacheableMetadata::createFromObject($access); $entity_metadata = CacheableMetadata::createFromObject($entity); $result = $result->merge($entity_metadata)->merge($access_metadata);This can be vastly simplified: we've had the
addCacheableDependency()method for a long time now. Also, the cacheability metadata for the entity is not necessary; the rendered entity's cacheability metadata implies this. -
$entity_output = $this->renderEntityEmbed($entity, $context);$entity_outputis a string. Which means that we've just lost the bubbleable metadata of the rendered entity.Or rather, we're relying on side effects to have it work: the virtue of
\Drupal\entity_embed\Plugin\Filter\EntityEmbedFilter::process()being called while within a render context is what ensures thatrenderEntityEmbed()(which callsRenderer::render()) bubbles the bubbleable metadata. However, the intent of the filter system'sFilterProcessResultis that that carries all the bubbleable metadata. - A very big one: inaccessible entities are still rendered…!
\Drupal\entity_embed\Plugin\Filter\EntityEmbedFilter::process()checks access, but then still happily renders inaccessible entities. This also means access is not checked for entities embedded using the Twig extension.Except apparently
EntityHelperTrait::renderEntityEmbedDisplayPlugin()does check access:// Check if the display plugin is accessible. This also checks entity // access, which is why we never call $entity->access() here. if (!$display->access()) { return array(); }But it returns the empty render array, which means cacheability metadata for access checks is lost. It also means that inaccessible entities are NOT rendered. But the logic is just extremely fragmented and almost impossible to follow because of that. Nevertheless, access check cacheability metadata for entities embedded via the Twig extension then are lacking cacheability metadata.
However, digging in further reveals that
FieldFormatterEntityEmbedDisplayBase::access()and surrounding code only deal with booleans, notAccessResultInterfaceobjects. This has therefore been wrong since September 2014 — see the following CR: https://www.drupal.org/node/2337377.
The second point actually points to another problem: EntityHelperTrait::renderEntityEmbed() returns a string, instead of a render array. But… it seems like that wasn't always the case. In fact, EntityEmbedTwigExtension::getRenderArray() still says it returns A render array from entity_view(). — despite it doing return $this->renderEntityEmbed(). Which is then a direct contradiction.
Proposed resolution
- Resolve all this by updating
EntityHelperTrait::renderEntityEmbed()to return a render array, and updating its docs. - This then automatically made the
EntityEmbedTwigExtension::getRenderArray()docs accurate again. - The above also makes
EntityEmbedFilter::process()receive a render array rather than a string. We then just need to render it correctly there and merge the bubbleable metadata explicitly.
Fixing the third point requires a major overhaul and should therefore probably happen in a separate issue.
Remaining tasks
None.
User interface changes
None.
API changes
None.
Data model changes
None.
| Comment | File | Size | Author |
|---|---|---|---|
| #48 | interdiff.txt | 719 bytes | slashrsm |
| #48 | 2593379_47.patch | 23.38 KB | slashrsm |
| #44 | interdiff.txt | 2.61 KB | slashrsm |
| #44 | 2593379_44.patch | 23.22 KB | slashrsm |
| #43 | interdiff.txt | 687 bytes | slashrsm |
Comments
Comment #2
wim leersPR at https://github.com/drupal-media/entity_embed/pull/187.
Comment #3
dave reidEven though we specifically used the following in the filter? I don't quite understand the point you're trying to make here.
Comment #4
dave reidComment #5
wim leersYes, the code you cite merges the cacheability metadata for the access result. But if it the access result indicates the entity is not accessible, the filter still proceeds to render the entity. After I'd written that, I noticed that apparently the "return something else in case it's not accessible" thing happens *elsewhere*. So yes, it *does* work. Which is why I wrote:
So for the filter, it's fine. But for the Twig support, it's not:
Comment #6
dave reidMaybe the renderEntityEmbed() method should have an optional BubbleableMetadata parameter and merges in the access data and the info from the render array?
How do we solve missing metadata when embedded with Twig?
Comment #7
wim leersHave the thing generating the render array do
Then everything is taken care of for you: cacheability metadata of the access check is handled, and not showing anything at all in case the entity isn't accessible is taken care of also.
Comment #8
dave reidOoh, renderEntityEmbed should totally do that!
Comment #9
wim leersI'll work on this in the beginning of January. Unless somebody beats me to it. If somebody does, please ping me on Twitter so I can review it.
Comment #10
slashrsm commentedComment #11
wim leersClosed #2712111: Embed specific attributes are cached along with the entity as a duplicate of this.
Without this issue solved, Entity Embed can never be secure (see IS for detailed analysis). I think this is blocking a stable release, so marking it critical.
Comment #12
slashrsm commentedThis is a re-roll of Wim's pull request.
I think that we should add test coverage for this.
Comment #13
slashrsm commentedIf I understand correctly this should address the problem nr. 3 in the issue summary. Also added test for that part which we don't have currently. We still need to add test for the cache metadata bubbling part.
Comment #14
dave reidI think the problem here is that we're now calling the rendering no matter if the user can access it or not. I guess that might be the point?
This behavior seems odd to me because of what happens currently in Entity Reference field formatters: EntityReferenceFormatterBase and EntityReferenceEntityFormatter. While the account for merging in the access metadata, the field formatters do not actually render the inaccessible entities. It really doesn't feel like we should not be calling the building if the access is not allowed.
I wonder if this could be fixed by ensuring the first time we call $entity->access() we ensure that the access metadata is bubbled up properly.
Comment #15
dave reidI think this is what I was more thinking, with moving the entity access check *only* to the renderEntityEmbed() method, which also adds the metadata of both the access and the render array itself. This removes the entity access checking from the display plugin, which I think actually makes a lot of sense, and shouldn't break any backwards compatibility.
Comment #16
dave reidBenefits of #15, there is now only one centrally located call to $entity->access().
Comment #17
slashrsm commentedAre you sure this really is a problem? It seems that renderer will early-return an empty string if it determines that the user doesn't have access to an element. If that is true then it doesn't really matter from performance standpoint.
If the above statement is true then I'd prefer #13 because we end up with simpler code. If I am wrong then I am OK with #15 too.
Would be great to get some feedback from @Wim Leers about this.
Comment #18
slashrsm commentedComment #19
slashrsm commentedNeeds reroll. Let's decide on the approach first.
Comment #20
wim leers#15: I'm not quite sure why you feel that #15 is simpler. Can you explain that in some more words? Can you explain what about #13 you dislike? I'd love to help address your concerns!
However, as far as I can tell, #15 has two significant flaws:
This means we still lose bubbleable metadata when something is not accessible (because we return empty arrays, which are render arrays, i.e. they don't have cacheability metadata that is bubbled, which means that if the first user to view this is a user without access, it'll be render cached without the necessary cache contexts & tags, which means users with access will also be able to see it). In both
renderEntityEmbed()andrenderEntityEmbedDisplayPlugin().This is wrong as I explained in the IS.
Comment #21
wim leersThis test coverage is not remotely sufficient.
This should verify that a user with access to unpublished entities should still see it.
This should also verify that upon publishing the node, this user can actually see it.
Finally, this should verify the necessary cacheability metadata is bubbled. That can be tested by implementing
\hook_entity_access()in a test module and associating nonsensical cache contexts & tags, and then you can verify that they're present. This would prove they are bubbled.Comment #22
wim leersThis is returning a boolean. This is going to be fine most of the time, which is why it hasn't been a problem yet.
But, as
\Drupal\Core\Entity\EntityTypeManager::__construct()indicates:The set of entity type plugin definitions is actually described by the
entity_typescache tag.So, really, the result of this is dependent on that cache tag.
Which is exactly why it'd be much better to just follow the standard of not returning booleans for access checking, but
AccessResultInterfaceobjects, which can carry cacheability metadata.Then that
entity_typescache tag could be passed along, because whether it's accessible or not is really dependent on that.(Look at
\Drupal\Core\Access\AccessibleInterface.)This should be called buildEntityEmbed(): it's not rendering, it's building a render array.
Same goes for
renderEntity()on this trait.renderEntityEmbed()is now building a render array with the necessary cacheability metadata. Great.That means the filter can actually stop worrying about this. The entity access checking can simply be removed from there.
Because the filter is just calling
renderEntityEmbed(), and that's already returning a render array with all the necessary metadata.Comment #23
wim leersIf all the feedback made sense to you but you find dealing with that cacheability metadata confusing, then I could take on rerolling this patch into the form that I think would make sense.
If so, it'd be great if the patch could first be rerolled though. I'm not confident I can resolve the current conflict correctly.
Comment #24
slashrsm commentedReroll of #13.
Comment #25
wim leersThis addresses #22.1 as well as possible. But it uncovered a big new problem.
EDIT: forgot to say what the new big problem is:
Comment #27
wim leersThis addresses #22.2. And simplifies the trait at the same time, because there is nothing left that uses the
rendererservice that the trait needed before.Comment #28
wim leersAnd this addresses #22.3.
Back to you!
Comment #30
slashrsm commentedThis should fix tests.
Comment #31
thenchev commentedExtended tests according to 21. User that has permission to view unpublished content is currently failing. Test if embeded content is displayed when its published is right below that, isn't that enough. The test for bubbleuble mettadata is not added yet
Also we have a bug here. When we try to embed a node the access check in ImageFieldFormatter trows an exception because of $entity->getFileUri()
Fatal error: Call to undefined method Drupal\node\Entity\Node::getFileUri()
Comment #33
thenchev commentedYea, sorry. addRole doesn't save the user. Tests are passing now. continuing with the rest.
Comment #34
dave reidThe context is referring to the individual embeds that are being rendered. So node 1 is the context of
In fact the whole data from the embed code is "context".
The only thing that's different is that a view mode is required. How does this lead to subtle bugs?
FYI here is the context documentation in case you missed it.
My main issue still and has always been that it seems logically very wrong to do all the rendering here, if the user can't even access it anyway. This change is not shared by ntityReferenceEntityFormatter::viewElements(). It merges the access metadata, but then does not render inaccessible entities. That's what I was trying to do in #15, merge the access metadata, but avoid rendering if no access. Why isn't doing that enough to ensure that when someone that cannot access a node should get a different cache than someone who can?
Comment #35
dave reidPlease don't remove existing @todos.
Comment #36
thenchev commentedBug fix and some test coverage.
Comment #38
slashrsm commentedNeeds doc block.
Agreed. We should create a follow-up and clean this up.
Needs doc block.
What exactly are we testing with this function? We need to improve tests or add a comment to explain the purpose of this submit.
Need to save user again.
Comment #39
thenchev commentedAddressed #38
added test for bubbleable metadata. Looking for some feedback on it.
Looks like EntityEmbedFilterTest is failing because of the access hook looking into it
Comment #41
thenchev commentedThis should correctly test the cache tags. Not sure about the cache context, there is validation that trows exception when I add nonsensical cache context. Still need to check why the embeded node is apearing when the user doesn't have permission.
Comment #43
slashrsm commentedThis should make tests pass.
Comment #44
slashrsm commentedFew nitpiks. I think this should be ready. All items from the IS were fixed and test coverage was vastly improved.
This will always be embedded entity, which should pass all its cache metadata when the render array is being built.
However, the naming is confusing a bit. It seems that the context array always stores just the embedded entity and machine name of its entity type. Should we rename/simplify this to reflect that?
However there is also
which seems to be a different context. It is added to the render array. We should at least update the hook documentation to make developers aware that they need to take care about cache metadata depending on how they alter the context. Can be a follow-up I think.
Comment #45
slashrsm commentedFollow-up created: #2760407: Clean up EntityHelperTrait
Comment #46
wim leersThis should not call
->isAllowed(). Well, because this is for anarray_filter()call, it's necessary. But it does mean that:So this means there are almost certainly edge cases where this will break.
It's been a while since I touched this, so I can't really judge the importance/impact of this. I'll leave it at RTBC and let the maintainers decide what to do. But IMO at least a
@todothere would be warranted.#34.4: But we don't do any rendering here. We merely build a very very very thin render array. That's also why I renamed it from
renderEntityEmbed()tobuildEntityEmbed(): it builds a render array, it doesn't render anything. This ends up in filtered content. The filtered content is part of an entity. That entity itself is render cached. And the cacheability metadata bubbled up from this#accessensures the right variations exist. So it's not like we do this repeatedly, we do this only once.That being said, you're absolutely right that what you describe is also possible. However, in your approach, it's literally impossible to override entity access. In the current patch, we first set
#accessand then call thehook_ENTITY_TYPE_embed_alter()hook, which can then modify#access. Either way is fine. But it should be a conscious choice. And we should have test coverage for it, no matter which of the two directions we choose.I can help with the early return if you want it to behave in that way.
Comment #47
slashrsm commentedAdded @todo. Output of
getDefinitionsForContexts()is used to determine which display plugins can be presented to the user in the embed dialog and are not cached. I think that we should be fine there?However, we should improve that to prevent any strangeness in the future, but that will require refactoring of things that are using the output.
Comment #48
slashrsm commentedPatch.
Comment #49
wim leersYep, that sounds okay.
Comment #50
slashrsm commentedCommitted. Thanks!
Comment #53
gábor hojtsyComment #54
geek-merlin