Support for Drupal 7 is ending on 5 January 2025—it’s time to migrate to Drupal 10! Learn about the many benefits of Drupal 10 and find migration tools in our resource center.
Problem/Motivation
It would be wonderful to be able to access the toLink()
and toUrl()
entity methods directly inside a Twig template (like label()
, id()
, get*()
, and a few others).
Steps to reproduce
Try to generate a link to an entity directly in a Twig template.
Proposed resolution
Add EntityInterface::toLink() and EntityInterface::toUrl() to the allowed functions in the Twig sandbox policy.
Remaining tasks
- Rescope the issue and implementation.
- Make sure the test coverage is sufficient.
- Reviews / refinements.
- RTBC.
User interface changes
None.
API changes
None.
Data model changes
None.
Release notes snippet
TBD.
Original report by @Chi
I suppose this has no security implications.
Comment | File | Size | Author |
---|---|---|---|
#74 | 2907810-74.patch | 7.78 KB | seanB |
#65 | 2907810-65.patch | 7.75 KB | seanB |
#60 | 2907810-60.patch | 8.1 KB | gabesullice |
#60 | interdiff.txt | 509 bytes | gabesullice |
#59 | 2907810-59.patch | 8.24 KB | gabesullice |
Comments
Comment #2
Chi CreditAttribution: Chi commentedComment #3
joelpittetSeems reasonable, here's the patch.
Comment #4
joelpittetMaybe while we're hear we should do EntityInterface::toLink() as well? Both aren't deprecated as are ::link() and ::url().
Comment #5
Chi CreditAttribution: Chi commentedI agree, toLink() is relevant.
Comment #6
googletorp CreditAttribution: googletorp as a volunteer and at Reveal IT commentedAdded patch which includes ::toLink.
Comment #7
Chi CreditAttribution: Chi commentedThanks.
Comment #8
Chi CreditAttribution: Chi commentedDoes this require a change record?
Comment #9
joelpittetYes could you create a change record for this @Chi?
Comment #10
Chi CreditAttribution: Chi commentedKindly review.
https://www.drupal.org/node/2908090
Comment #11
googletorp CreditAttribution: googletorp as a volunteer and at Reveal IT commentedRegarding change record, i believe there is a minor space issue:
Should be
This is a minor CS issue (there is no space in twig when using filters between |).
Comment #12
Chi CreditAttribution: Chi commentedFixed space issue.
Comment #13
larowlanHi, I agree with this one, normally do it on my sites anyway.
But we need a test here, otherwise we could easily lose it without knowing.
You an add to the existing
\Drupal\Tests\Core\Template\TwigSandboxTest::testEntitySafeMethods
Comment #14
jofitz CreditAttribution: jofitz at ComputerMinds commentedAdded tests. No interdiff because the only change is in the test_only file.
Comment #15
dawehnerNote: the expected result should actually be the first parameter ...
Comment #16
jofitz CreditAttribution: jofitz at ComputerMinds commentedThat is a very valid point, thanks @dawehner. I had simply copied and edited the existing tests. I have made the correction for my new tests, but left the existing tests as is because that is not part of the scope of this issue (but I am happy to make the change, if required, or create a new issue to address this).
Comment #17
dawehnerYeah let's not touch existing tests
Comment #19
Wim Leersentity.toUrl()
+entity.toLink()
, and notentity.url()
+entity.link()
? The latter seems a bit more logical?Comment #20
Wim LeersOh and
Comment #21
Wim LeersComment #22
Chi CreditAttribution: Chi commentedurl() and link() methods are deprecated.
https://api.drupal.org/api/drupal/core%21lib%21Drupal%21Core%21Entity%21...
Comment #23
joelpittet@Wim Leers,
Comment #24
Wim Leers$entity->toUrl('edit-form')
Addressing this a follow-up is IMO dangerous — because once the API is added, it can't be changed anymore.
Comment #25
joelpittetgetUrl()/getLink()
and it wouldn't need whitelisting and slightly better TX{{ entity.getUrl() }}
, or is that just putting more cruft?{{ entity.toUrl('edit-form') }}
, I bet this is not the case in D8 but yeah I guess I should test that here too.Also, maybe we should double check the output for Exceptions like
EntityMalformedException
andUndefinedLinkTemplateException
to ensure the templates don't explode in horrible ways?Thanks for the review @Wim Leers, sorry didn't say that in #23 but much appreciated!
Comment #26
Wim LeersThis is why it's important to discuss this here first, agree on how it should work, and have tests to prove it works this way. There's a security aspect to it too!
Yep, you're right — if I do
{{ entity.toUrl('non-existent-link-relation-type') }}
then it will indeed fail. We need to have tests to ensure a good TX in this case.Comment #27
dawehnerMh, so this would 3 methods on the entity objects all doing mostly the same. I guess this would treat DX against TX. Maybe we could provide something like
{ entity | url }
or so?Regarding point 3, its an old discussion, but in that case we should fix Entity.toLink. I remember its not the first time we have this discussion.
Comment #28
Chi CreditAttribution: Chi commentedWe already have whiltelisted ::label() and ::getTitle() methods which don't care about entity access. I think checking access is up to developer/themer. We just giving them more convenient way of getting entity URL in templates.
Comment #29
jofitz CreditAttribution: jofitz at ComputerMinds commented@Wim Leers In which file should I put the test coverage discussed in #19-2?
Comment #30
cburschkaAs we're only mocking the method, does this just mean adding a test that tries
{{ entity.toUrl('edit') }}
instead of{{ entity.toUrl }}
and checking that the argument is passed to the mock?Comment #32
No Sssweat CreditAttribution: No Sssweat commented#30 worked perfectly for me.
Comment #33
dawehnerDo you mind actually returning something which is a link? Right now it is just some sort of random string :)
Comment #34
jofitz CreditAttribution: jofitz at ComputerMinds commented@dawehner do you mean a Drupal\Core\Link or something that looks like a link, e.g. '/this/is/a/link' or 'http://drupal.com'?
Comment #35
dawehner@Jo Fitzgerald Yeah something which is more of a realistic link. A link should be something like
<a href="/node/123">This is a node</a>
Comment #36
cburschkaI just tried this; If we want to return actual Link or Url objects here, we will need
- Drupal's Twig extension to convert them into strings. Fortunately it looks like all its service dependencies can be empty mocks.
- The unrouted URL assembler, to actually build the URL.
- A mocked request stack (which the assembler uses to get its base path).
It's probably not impossible (I got as far as point one), but there seems to be some some non-trivial mocking involved.
(Note that simply adding Drupal's twig extension in ::setUp also causes some other side effects, such as failing the ::testExtendedClass test.)
Comment #37
cburschka(Cross-post)
Comment #38
jofitz CreditAttribution: jofitz at ComputerMinds commentedReturn links (and URLs) as requested by @dawehner in #33.
Also removed redundant, erroneous messages.
Comment #40
moshe weitzman CreditAttribution: moshe weitzman as a volunteer commentedThanks for the tip about the method whitelist. I'd love to see toUrl added by default, as proposed here.
Comment #42
noel.delacruz CreditAttribution: noel.delacruz commentedQuestion, will this patch be added to the Core anytime soon? Since every new update of Drupal I have to re-apply the patch over and over again.
Comment #43
Wim Leers#42: Good question. This has gone under the radar for a long time.
Looks like #19.2's reason for was addressed in #30. Except it's just returning different values for consecutive calls, so it's not really testing this. But I think this is probably good enough nonetheless: testing this better requires setting up a lot more test infrastructure, and we don't have that elsewhere in
\Drupal\Tests\Core\Template\TwigSandboxTest::testEntitySafeMethods()
either.Fixing CS violations and RTBC'ing. Let's see if front-end framework manager agrees this is sufficient.
This will still need a change record though.
Comment #44
joelpittet+1 to add this from a theme system subsystem maintainer FWIW;)
Removing tag for CR because it's here: https://www.drupal.org/node/2908090
Comment #45
Wim Leers#44: YAY! (And oops 😊)
Updated CR to say "Drupal 8.8".
Comment #46
noel.delacruz CreditAttribution: noel.delacruz commentedThank you for this, really appreciate the effort for pushing this to the core. Looking forward to it 😊
Comment #47
Wim LeersIn #26.3 I mentioned security aspects, but I missed one so far.
There's a security risk in doing this, because:
and
and last but not least, fortunately in
\Drupal\Core\Utility\LinkGenerator::generate()
:… but due to the mocking of
toLink()
(which I also called out in #43, we're not verifying that<script>alert('xss');</script>
as the firsttoLink()
parameter is being processed appropriately.You could argue that it'd be stupid for a front-end developer to pass in a script tag, but there's no guarantee that they're not passing in user input.
Comment #48
noel.delacruz CreditAttribution: noel.delacruz commentedIs there any progress of having this patch push to the core?
Comment #50
Chi CreditAttribution: Chi commented+ $entity = $this->getMock('Drupal\Core\Entity\EntityInterface');
getMock() is deprecated and needs to be replaced with createMock().
Per #47 we need a separate integration test.
Comment #51
neclimdulugh what a twisty rabbit hole this sent me down... And I just wanted to dump an node link in a twig template :(
First the easy stuff. Ugh mockery mocks... But I think Wim's comments in #47aren't a big deal and mocks here are probably fine. We don't have to assert that
TwigExtension
renders Link's and Url's safely. We should already be confident of that based onTwigExtension's
tests because even if we can't calltoUrl
andtoLink
directly from twig, passing links and urls to twig files is common practice. And in factTwigExtensionTest
has some basic assertions around GeneratedLink and Url so I _think_ we're ok.The rest is a little ranty, because I started digging into the Url and Link classes... Sorry.
Confirming my understanding of how Link and Url would render lead me down the real rabbit hole though, passing through such issues as #2491981: There are too many ways to generate URLs and links and #2529560: Expand support for link objects. I made the naive assumption that Drupal would render Links and Url's in twig the same way. Probably throwing them into render array's and then churning that through the renderer.
What I found was Links do have
RenderableInterface
,TwigExtensions
picks that up, and then through some twisty turny render array logic we get a rendered link out ofLinkGenerator
. Pretty much exactly like expected. Link also has a much simpler toString method that skips all the logic in Element\Link but still gets the escaping in LinkGenerator so that's safe too. Why the difference, who knows but the render array works so we're fine.Where I was really surprised was with
Url
. It _has_ a render array method but its named different and ends up not actually implementingRenderableInterface
soTwigExtension
falls through to callingUrl::toString
. Since its not the interface method I wonder if its even used or just cruft of some bygone idea(follow up, it is only used by shortcut.module in core).Anyways, the real WTF is like
Link
,toRenderArray
andtoString
methods behave different. If we where getting the render array it would be wrapped in an access callback andtoUrl()
would follow Wim's expectation and apply an access check. But callingtoString
like we currently do means we get Joel's expectation. Truely we still have too many ways to many ways to generate links and URLs... or more accurately don't have a consistent way :(Comment #52
neclimdulAs payment for that long rant, here's an updated patch addressing #50. I just converted them to prophecy because with all the new assertions it makes the test shorter with less repetition and a heck of a lot easier to follow. Also I'm not a fan of Mocks. :-D.
I also added an assertion that link handles both passing null and a string correctly since it was trivial and there's a couple phpcs fixes and a couple fixes to the older assertions shoved in there too. The argument order was reversed and gave incorrect failure messages. Sorry to the kittens but it was getting hacked up anyways. :-/
Comment #53
neclimduloh... moving to 9.1.x for targeting since that's where it would currently land.
Comment #55
phenaproximaI gave this patch a test and indeed, it lets us use
{{ node.toUrl }}
in a template quite nicely. (Considering that all entities implement toUrl(), this is a wonderful, generic way to print out any entity's URL in a template, and it solves #3184316: Add entity_url Twig function for easy generation of entity URLs, which I am closing as a duplicate.)Unfortunately, if you try to make that an absolute URL -- for which I can absolutely imagine some legitimate use cases, as @webchick alluded to in #3184316-7: Add entity_url Twig function for easy generation of entity URLs -- it fails with an exception:
This happens whether you do
{{ node.toUrl.setAbsolute }}
or{% set absolute_url = node.toUrl().setAbsolute() %}
. (Obviously the former would be far more preferable from a themer perspective.)So I think we almost certainly want to fix that, or it will be a pretty big WTF for themers who want to print out absolute URLs. Therefore, I'm kicking this back and tagging for the various administrative changes it needs before it can be committed.
Comment #56
phenaproximaRe-titling, since we have retired the terms "whitelist" and "blacklist" in core.
Comment #57
phenaproximaI'd like to preserve my RTBC privileges, so I won't be the one to implement this...but one way we could fix #55 is to allow TwigSandboxPolicy::$allowed_methods to also contain fully-qualified method names, like
Drupal\Core\Url::setAbsolute
, and modify checkMethodAllowed() to search for those as well.Comment #58
larowlan+1 for setAbsolute, and also setOption - provided it doesn't derail the scope too much, those are definitely needed to make this useful
If that requires a fairly major pivot, lets do those in a followup
Comment #59
gabesulliceI re-rolled this for the 9.2.x branch.
I also implemented
setAbsolute
and added the ability to add fully-qualified names... which is probably how it always should have worked, so I added a deprecation notice for not using FQNs.I did not implement
setOption
because:{{ entity.toUrl }}?myQuery=param
I left all the
toLink
changes in the patch, but I'm not convinced thattoLink
is needed/wanted.The
htmlentities()
method in the tests has me worried because I'm afraid that it's going to render as escaped HTML rather than an actual hyperlink and that that is not the expected behavior. I'm not a render/twig expert though, so maybe it's fine.Comment #60
gabesulliceWhoops, this was not meant to be in the patch. However, this does remind me of a point I forgot to make in my last comment...
It's really difficult to test the FQN thing since I don't believe that the Twig system runs the security policy code if the method doesn't exist and since
EntityInterface
andUrl
don't share thesetAbsolute
method, the above does not return an error as expected.Comment #62
neclimdulThere's a lot of good things going on here but the scope seems to be expanding quite a bit which doesn't feel right. We've got a entity sandbox changes, url sandbox changes, and now deprecation to sandbox behaviors. They're all kinda bundled up in the same place but its harder to discuss the impact of the changes. Think of the kittens.
That said, I like the fully qualified method change. There is still some security concerns from Wim that I'm not sure are fixed but this does feel like it buttons up some things that didn't feel right.
Comment #63
phenaproximaI agree with @neclimdul and suggest that we dial the scope back a bit here. I propose:
get
,has
, etc. prefixes.Kicking back to "needs work" to readjust the scope.
Comment #65
seanBReroll for 9.2.x.
Comment #66
neclimdulIs this possibly breaking? I think you're right that was the intention but if I had my own object I was throwing into twig and calling
.id
on because it just worked then it would break right?Comment #67
dwwBig +1 to this. It would make many things much cleaner and better.
There was already a CR for this, and I just gave it some updates, so removing that tag.
Haven't yet read the full issue history, nor reviewed the code.
Comment #68
dwwGave this a proper summary, removing that tag, too...
Comment #69
neclimdulopened #3239105: Harden TwigSandbox methods with the hardening component of the current patch to discuss the impacts and solve some issues with the current patch.
Comment #74
seanBReroll for 10.1
Comment #75
longwaveJust run into this again after an internal discussion where we debated Twig vs PHP:
or
I couldn't believe there wasn't a simpler way, and then I found this issue again.
I would also note that the
url()
Twig function already assumes all URLs will be absolute:I think we could sidestep this and the BC and deprecation issues if we just assume that a method name (without
::
) also means "any class"?