Follow-up to #2417793: Allow entity: URIs to be entered in link fields
Problem/Motivation
- In Drupal 7, Menu UI allows creating external links (links that start with http: or other trusted protocol), but disallows creating internal links to paths that don't exist or that you don't have access to (unless you have the "link to any page" permission).
- Drupal 8 HEAD still has that behavior, but #2417793: Allow entity: URIs to be entered in link fields moved that from a field-level constraint to a widget-level decision.
- AFAIK, there are no longer security requirements for that behavior. In Drupal 7, this was done because we resolved aliases during submission, so there could be information disclosure without this. But now, we just store what the user typed, so no information disclosure.
- This behavior prevents you from making a link to a resource on the same site that isn't served by Drupal or to a resource you plan on adding later, but meanwhile want to build out your menu.
- Once #2418017: Implement autocomplete UI for the link widget is in, this behavior also won't offer any UX benefit in terms of helping you avoid typos. Since the autocomplete will serve that purpose.
Proposed resolution
- Remove this validation during form submission time (i.e., from the widget). Perform access and other requirements checking during menu/link render time only.
- The 404 and 403 cases are covered together here, so that we are not disclosing whether a resource that you don't have access to exists or not.
- The allowed external protocol validation on input is not being removed in this issue. That should be a separate discussion.
Remaining tasks
TBD
User interface changes
TBD
API changes
Beta phase evaluation
| Issue category | Task, its part of the major menu overhaul |
|---|---|
| Issue priority | Major, because its a big improvement for site builders |
| Disruption | Not disruptive, because it doesn't change any API, just allows more than before. |
| Comment | File | Size | Author |
|---|---|---|---|
| #53 | interdiff.txt | 2.62 KB | dawehner |
| #53 | 2418181-53.patch | 19.42 KB | dawehner |
| #51 | interdiff.txt | 12.34 KB | dawehner |
| #51 | 2418181-51.patch | 19.33 KB | dawehner |
| #48 | 2418181-48.patch | 13.05 KB | dawehner |
Comments
Comment #1
effulgentsia commentedThis is changing behavior, so should trigger failures of tests for the old behavior.
Comment #3
wim leersShouldn't this also remove this code in
\Drupal\link\Plugin\Field\FieldWidget\LinkWidget::formElement()?Comment #4
effulgentsia commentedYep.
Comment #6
wim leersFixing the test failures.
Comment #7
wim leersShould be green now.
Comment #8
wim leersAnnotated code to explain the changes in
LinkFieldTest, because they'll be hard to understand otherwise:We now allow non-existing paths to be entered!
This is no longer invalid.
This never was invalid, merely disallowed, so adjusted accordingly, so that it's no longer misleading.
We now allow any path, but this didn't cause test failures because it never was actually tested (see next bullet).
This is a bug ever since this test coverage was introduced in #2054011: Implement built-in support for internal URLs… and yep, that means that nothing in
$invalid_internal_entrieswas ever tested, and that the first case of$valid_internal_entrieswas never tested.These
array_merge()s fix that.All of these, when prefixed with
user-path:, are actually valid URIs. Hence removed this test case.Comment #9
dawehnerJust to be clear, we certainly have to deal with the following situation:
User entered entity:node/1 and you see the node title immediately (once we have autocompletion).
Comment #10
dawehnerI kinda think that depending on the site people might want to have the 404/403 behaviour, it is also partially a UX improvement to know that you link to a non-existing thing.
Given that we could make the access/validation level an additional setting on the field level itself.
Comment #11
wim leers#9: that's a superbly excellent remark. Thoughts:
entity:URIs that are inaccessible itself also doesn't expose the title. What you're referring to is only actually a problem when we have the entity autocomplete widget. And we could still make it so that the entity autocomplete widget doesn't autocomplete inaccessible URIs. Then there is no harm.Comment #12
wim leers#10: Agreed that that would be a valuable thing to be able to configure.
Waiting for others to chime in regarding that. If we agree we want that, then we'll have to decide whether to make it a field setting or a widget setting. If it'd be a field setting, we can move the logic into
LinkTypeConstraint. If it'd be a widget setting, we conditionally execute the existing logic.Comment #13
dawehnerThere is another point: If you allow people to link to stuff they don't have access to, but also doesn't exist, they can't edit those links anymore,
as they would be hidden from the menu UI.
... removing the access checking is certainly not possible.
Comment #14
wim leersdawehner and I had a discussion about whether doing what this issue is currently doing — removing all 403/404 validation from the UI — has other repercussions. Turns out it does: it'd either lead to unavoidable information exposure in the menu UI, or to the user not being able to see the links he just created.
Hence I think we really need further discussion about the plan in this issue's summary.
Comment #15
pwolanin commentedSo the case in question is mostly where a user links to a path?
Can we fall back to showing the path (the user input) instead of the label?
I guess the problem is that if someone else entered the path alias that contains the title, we don't want to show that either?
Comment #16
wim leersIn the widget: definitely.
In the menu tree UI: it'd only be possible if for every link that the user cannot access, we render the URL as either the empty string or the stored URI, without resolving the URI to an URL.
Comment #17
wim leerspwolanin in chat also thinks this should be a widget setting. Marking NW for doing that.
Comment #18
pwolanin commentedActually - a field setting would make more sense if that's possible - so if I change the widget I don't lose the validation
Comment #19
effulgentsia commentedWe discussed this in IRC today, and the consensus there was that core shouldn't be constrained by this, because in core, you can learn whether something is a 403 rather than a 404 by typing it into your browser. A site that wants to prevent that could do so by implementing a 403 exception handler that returns a 404, but such a site can also write code or install a module that changes link field validation to the desired behavior.
We also decided that we want to preserve HEAD's current functionality of preventing people from creating links to something that exists but to which they don't have access. Because, it can be common for administrator 1 to create a menu link to something sensitive and also give that link a sensitive title. And we don't want administrator 2 to then be able to see that title within the Menu UI. Which we accomplish by hiding inaccessible links from the Menu UI. But, if we're gonna hide them in the Menu UI, we should also prevent you from making them, since otherwise, you can create a link that you then can't find or edit.
So, I suggest we rescope this issue to just removing the 404 validation, and open a new issue to move the 403 validation from the widget to a field-level constraint.
Comment #20
dawehnerOpened up a new critical issue, given that its a security problem to not check access.
#2419693: Move URI access validation from widget to field constraint
Comment #21
webchickMy 2 cents as someone who's attended multiple UX studies and seen users smashing their faces into tables trying to do what should be basic CMS 101 stuff with Drupal menus... the main use case we're trying to support with this issue is:
"As a site builder, I want to create the information architecture of my site by building the entire navigation structure out first, before populating it with content."
This is allowed by every other HTML authoring tool and CMS I've ever used—including Drupal, up until Drupal 6. So IMO, bypassing 404 checking only is fine (this is what I would expect System module's "link to any page" permission to do, instead of what it currently does around 403s), because that is the crux of the regression we introduced in D6. (If there are other use cases out there, I am not aware of them, and I'm guessing they are definitely not as important to support as this one.)
Comment #22
davidhernandezI don't follow the administrator 1, administrator 2 scenario. Don't both these admins have "Administer menus and menu items" permission? I would think most people would assume that means they should see all the links and have the ability to administer them. I don't know how to reconcile that with information disclosure of the node titles, but I will say that I don't think I've ever had a menu admin that wasn't a content admin. I consider administrating menus a higher level privilege.
Comment #23
pwolanin commented@davidhernandez - I tend to agree with you in practice, and it would make the logic easier if we could just bypass access checking for those admins.
However, one use case we discussed was where there are menu admins who maintain different parts of an intranet site (different departments?) and where information should not be able to see information restricted to the other sections.
Or alternatively, a menu admin who is supposed to be updated the public/anonymous facing menu for a site, but shouldn't be able to see content titles protected by node access.
Comment #24
wim leers#22: indeed. What we need to really solve that is … gasp … hierarchical permissions. That's definitely D9.
#23: that almost makes the case for
Menuentity access control handler (perhaps implemented as a permission generated automatically for every menu, just like what we do for node bundles.).Comment #25
webchickWhy does core need to care about any of those use cases? That'd be for Fancy Menu Permissions Module in contrib.
Comment #26
effulgentsia commentedThere's no need to validate 404 on output, right? If you want a link to /blog, but that's a URL handled by Wordpress, we should let you do that and show it in the rendered menu too, right? Retitling issue accordingly, but correct me if I'm wrong.
Comment #27
webchickYes! The issue title is all I've ever wanted since 2008. :) Thanks.
Comment #28
dawehnerWell, let's say there is no performant way.
Added some first basic change. One general question is whether a link field with type INTERNAL is meant to be internal or just relative to the Drupal installation directory?
Comment #30
effulgentsia commentedI think it should mean
!$url->isExternal(), and that isExternal() should return FALSE forbase:URIs as is already the case in HEAD.Comment #31
effulgentsia commentedIn other words, I think we should follow this definition from http://en.wikipedia.org/wiki/Internal_link:
That page proceeds to then question the "or domain" part, but I think the "same website" part is a good standard, and at a minimum, I would consider every page within Drupal's installation directory to be on the same "website", even if that website consists of multiple applications, like Drupal and Wordpress.
Comment #32
dawehner@effulgentsia
Good points. Especially I think this is somehow what the user expects to be available.
Let's just quickly fix the failure.
-&-now seems to be an allowed URI.Comment #33
yched commentedThat's kind of a double negation - wouldn't it be clearer if we moved to $allowed = TRUE ?
Comment #34
dawehnerWell, we still need the full chain of arguments and then need a negation at the end for the constraint violation. But yeah, its maybe worth to try it out once.
Comment #35
wim leersROFL!
#2419693: Move URI access validation from widget to field constraint landed! As have other patches. #32 no longer applies.
Feedback: I wonder if we want to move this into a constraint as well? I.e. move the existing logic into a constraint, but don't use it by default. But that way, it's unit tested, and anybody who wants to disallow 404s, can just enable that constraint.
Comment #36
dawehnerAlright, let's start from scratch.
Comment #38
dawehnerMh, that DX is weird.
Comment #40
dawehnerLet's see.
Comment #42
dawehner.
Comment #44
dawehnerBoolean logic is hard to get right on the first time.
Comment #46
dawehnerAlright.
Comment #47
wim leersThis patch is looking *awesome*! :)
To avoid doing this, effulgentsia and I added
\Drupal\link\Plugin\Field\FieldWidget\LinkWidget::flagErrors()to replace the@urivalue dynamically in the widget :)See commit
089a68f4a8453911df31656f04c48f9f0e410527, issue #2412509: Change LinkItem.uri to type 'uri' rather than 'string' and introduce user-path: scheme.i.e. this should not be necessary.
s/an/a/
What about "dangerous" instead of "harmful"?
Hrm, do we want the comment to be on its own line, above the catch-statement?
Comment #48
dawehnerAh this is cool. Thank you!
Sure, let's do that.
Comment #49
yesct commented(applies, removing needs reroll tag)
Comment #50
wim leersThis is now no longer necessary either.
Other than that, this code looks ready. But I think we want unit tests for each constraint, much like Upchuk did in #2419693: Move URI access validation from widget to field constraint for
LinkAccessConstraint?Comment #51
dawehnerThere we go.
Comment #52
wim leersWOW, that was fast!
Really only nitpicks, but I believe they can make a difference in long-term maintainability: it'd make the tests and therefore the code easier to grok for future readers.
After this, RTBC.
Nit: Invalid.
This is confusing.
The function name suggests this is an invalid URL as in "considered invalid by this constraint".
But it's really an invalid URL as in considered invalid by the
Urlclass, which only happens in case of a schemeless URI.So I think this should be named
testValidateWithMalformedUri.It could also use an
@see Url::fromUriI think a name like
testValidateIgnoresInternalUrlswould be a clearer name.Nit: Let's remove one blank line, so we have 1, not 2.
Nit: inconsistent code formatting impairs legibility. Let's have a blank line before each comment at least.
Comment #53
dawehnerFixed the nits.
Comment #54
wim leersComment #55
alexpottCommitted 1a72848 and pushed to 8.0.x. Thanks!
Thanks for adding the beta evaluation to the issue summary.
Comment #57
tstoecklerThis is probably a stupid question, but how does the committed patch actually reflect the issue title?
I see a
LinkNotExistingInternalconstraint being added toLinkItemso it seems we do validate that you cannot enter a non existing path? I also don't see tests for successfully adding a link to e.g./wp-content/why-drupal-sucks(sorry, couldn't resist).Comment #58
effulgentsia commentedAgreed with #57. From #35:
Looks like #53 did not implement that "don't use it by default" part, so resetting this to Active. Not sure whether it's better to proceed in this issue or open a new one, but even if the latter, I think this should remain Active until that issue is filed.
Comment #59
effulgentsia commentedActually, this does work. I don't know if we have a test that explicitly checks for it, but some tests were removed that checked otherwise, such as:
The reason it works is a bit subtle:
Note that in the above, this constraint only adds a violation if $url->isRouted(). Whereas Url::fromUri('internal:/not/a/drupal/path') returns a $url object whose isRouted() is FALSE. I don't know if this was the intent of the constraint though. The name of the constraint is LinkNotExistingInternalConstraintValidator, so if that's correctly named, then it should probably also be flagging a violation if
!isExternal() && !isRouted(), though if we make the constraint do that, then we'll want that constraint not added by default. Or, should we leave this constraint as-is, but then rename it to be accurate, such as LinkIfRoutedThenValidRouteConstraint or similar?Comment #60
wuinfo - bill wu commentedMy 2 cents: we keep the 404 validation from link creation and leave it for a contributed module to do the job.
Comment #61
webchickThis was already committed awhile back, and seems to work as indicated from manual testing (which made me openly weep with happiness), so moving to fixed. If there are other things to be discussed, we should do so in follow-up issues.