Problem/Motivation
When a user is not allowed to access any contextual links, we still render a contextual dropdown, but without any links in it. This is very confusing, bad UX.
Proposed resolution
Root cause analysis:
- User starts with a fresh browser tab. User loads page.
- We (==
contextual.js) check the cache, we do:
var html = storage.getItem('Drupal.contextual.' + contextualID); if (html !== null) { // cache hit, yay!If there's a cache miss, then
getItem()returnsnull. - We had a cache miss, so we must request it. We iterate over the results in the response, we do this for every requested contextual link placeholder:
if (html.length > 0) {— and only if some HTML is returned for a particular contextual link (i.e. if the current user has access to any of the contextual links), we callinitContextual(). This is the intent. - User navigates to another page.
- We again check the cache:
var html = storage.getItem('Drupal.contextual.' + contextualID); if (html !== null) { // cache hit, yay!Now we have a cache hit! So we call
initContextual(). But… we call it blindly. We forget to check if the current user has access.
(Look at \Drupal\contextual\Tests\ContextualDynamicContextTest::testDifferentPermissions(), you'll see that an empty string being returned for a given contextual link ID is indeed intended to return the empty string when it is inaccessible.)
Conclusion: when using the client-side cache, we must not only check html !== null, but also html !== '' (or html.length > 0.
Remaining tasks
Test coverage!
User interface changes
None, except a no longer broken UI.
API changes
None.
Data model changes
None.
| Comment | File | Size | Author |
|---|---|---|---|
| #72 | interdiff.txt | 589 bytes | GrandmaGlassesRopeMan |
| #72 | 2650910-72.patch | 2.83 KB | GrandmaGlassesRopeMan |
| #70 | interdiff-2650910-64-70.txt | 1.53 KB | markdorison |
| #70 | contextual_links_button-2650910-70.patch | 2.84 KB | markdorison |
| #64 | interdiff.txt | 1.03 KB | GrandmaGlassesRopeMan |
Comments
Comment #2
tom robert commentedComment #3
tom robert commentedComment #4
tom robert commentedComment #5
tom robert commentedComment #6
anishnirmal commentedThe patch drupal_core_contextual-empty_contextual_filters-2650910-8.1.x-dev.patch at #4 works.
Comment #7
alexpottThe patch looks good but as this is a javascript change and we have no test infrastructure in place we need some screenshots as test evidence.
Comment #8
anishnirmal commentedHi, Please refer the below screenshots
Admin user with the contextual links

Authenticated user with Contextual link Permission enabled - Before patch

The Link icon exist without sub-links
Authenticated user with Contextual link Permission enabled - After patch

The Link icon not exist without sub-links
Comment #9
anishnirmal commentedComment #11
dbjpanda commentedComment #12
anishnirmal commentedHi,
Created the patch for D8.2.x-dev version. Below i have attached the before and after screenshots and it works fine
Before Patch:

After Patch:

Comment #13
wim leersThis is wrong. This won't work if the content contains any link, including non-contextual links
Comment #14
tom robert commentedThe html element contains only the html for the contextual links
So in the html there is only something like this:
So if html doesn't contain a link don't render the links.
Comment #15
wim leersYou're right. Sorry. I misread the code (it's been a long time since I've seen this!).
But… your code is still misleading. The right thing to do is checking if
htmlis empty or not. Because if no links are accessible, thenhtmlwill be the empty string. Verify it in your debugger.Also, #2469713: Step 2: Create a JavaScriptTestBase using PhantomJs Driver/Binary has landed, so now we can write test coverage for cases like this! Look at
core/modules/toolbar/tests/src/FunctionalJavascript/ToolbarIntegrationTestfor an example.Comment #16
tom robert commentedThat is indeed correct. I thought that when i tested it i received an empty
<ul>. But this is not the case anymore so an empty check should suffice.Comment #17
wim leersCool :) Thanks! And thanks for helping to fix this!
Comment #18
psend commentedIf the user do not have permission to access the associated links then contextual links are empty.
It is better to check if contextual links are not empty after we get them from the sessionStorage.
As it is checked after the Ajax request:
Comment #19
psend commentedComment #20
dpiAdding related issue since this issue does not have good Google juice.
Comment #21
markdorisonPatch in #18 works as expected.
Comment #22
droplet commentedthe code that handling `storage.setItem` & `storage.getItem` should be same logic. Unbalance coding confused developers. Also considering the shortened way to check value exist: `if(var) { ...}`.
Comment #23
wim leersThe patch in #18 is exactly right.
contextual.js) check the cache, we do:If there's a cache miss, then
getItem()returnsnull.if (html.length > 0) {— and only if some HTML is returned for a particular contextual link (i.e. if the current user has access to any of the contextual links), we callinitContextual(). This is the intent.Now we have a cache hit! So we call
initContextual(). But… we call it blindly. We forget to check if the current user has access.(Look at
\Drupal\contextual\Tests\ContextualDynamicContextTest::testDifferentPermissions(), you'll see that an empty string being returned for a given contextual link ID is indeed intended to return the empty string when it is inaccessible.)Conclusion: the patch in #18 is entirely correct.
So -1 for #22.
However, this still needs JS test coverage.
Comment #24
wim leersActually, even better would be to test
html !== '', becausehtml.length > 0would e.g. also allow arrays/objects.Updated IS with #23's analysis.
Comment #25
xjmBumping to major in light of #2362435: When viewing a revision, the Quick Edit, Edit, and Delete contextual link operations are available, but should not be.
Comment #27
wim leers#2047671: Expanded contextual links look goofy when there are no items fixed this for the non-cached case. This is fixing it for the cached case.
Comment #28
yannickooWhat do you think about this condition like this:
typeof html === 'string' && html.length > 0?Comment #29
yannickoo(And one question: Does the core prefer
foo.length > 0instead of justfoo.lengthfor readability reasons?)Comment #30
flodevelop commentedThank you for the patch, it works for me.
Comment #31
othermachines commentedPatch in #29 fixed the problem of the contextual button displaying when no contextual links were available (Drupal 8.2.3). Thanks!
Comment #32
rachel_norfolkThis looks *really* simple solution and the attached unpatched and patched screenshots show it is effective. Marking RTBC...
Comment #34
xjmAs per #23 this still needs test coverage. Thanks everyone!
Comment #35
GrandmaGlassesRopeManTests.
Comment #37
wim leersYAY!
Comment #38
rachel_norfolkThis is a bug and the "benefits outweigh the disruption". We are still in 8.3 beta so I think this can be applied to 8.3
I was going to say I was too timid to change it but that's something I'm consciously trying to change!
Please correct if I'm wrong.
Comment #39
wim leersNope, you're absolutely right. And even if 8.3.0 was already released, this would still be able to go in to 8.3, because it's simply a bugfix without other consequences.
Comment #40
droplet commentedI disagreed with #23, +1 for #22
First,
It patched the LINE 166 with:
But LINE 195:
Unless we make assumptions there, I don't know why we end up with 2 different conditions.
Second,
Thrid:
Storage interface returns DOMString, or null.
Comment #41
droplet commentedalso,
LINE 191:
Should we save empty HTML then?
Comment #42
GrandmaGlassesRopeMan@droplet
Thanks for pointing that out. I updated the logic to check for a non-null element and that the element has a length.
Comment #43
wim leers#41: yes, saving the empty HTML is intentional: that means the current user cannot access those particular contextual links. It's pointless to re-request them over and over again. If you want to change that, then let's do that in a separate issue.
#42: See #24 for why I suggested checking if it's a string. It was
typeof html === 'string'until #42, but I thinkhtml !== ''would have been okay too.That's no longer being checked in #42. Are you sure that this is okay?
Comment #44
GrandmaGlassesRopeMan@Wim Leers
It's my understanding that the interface for
window.sessionStorageis to always return a string when requesting an item. Here we are checking to see thathtmlexists, not null, and that it has a length; which in weird JavaScript land, the result of checking the length of the string will result in atruecase if that value is non-zero.Comment #45
wim leersYes, but in this location,
htmlis not read fromsessionStorage. So it might be an object or an array — which is what I wrote in #24.Comment #46
GrandmaGlassesRopeMan@Wim Leers
If there’s ever a possibility that the result of the AJAX call to
contextual/renderwill return something that is not a string then we need to expand the checking.Comment #47
wim leersI don't see how that can happen, so then this is fine. Still, by that same logic, we shouldn't even be modifying line 196. Because
htmlcannot ever benullwhen parsing a successful AJAX response. (In other words: by that same logic, #23's argument is invalid.)Anyway, this is fine. Let's get this in. Finally.
Comment #48
droplet commentedYes, it is. (or a chance returned false or null?) Basically, I suggested
if (html) {}but if we end up with current patch. It's still fine.Comment #49
alexpottThis test is only a negative test - is there any chance to add additional tests to ensure contextual links are there in the right conditions?
Comment #50
imiksuComment #51
duaelfrFixing event tag :)
Comment #52
GrandmaGlassesRopeManAdding an additional test step from #49.
Comment #54
GrandmaGlassesRopeManNeed to wait for ajax to complete.
Comment #56
GrandmaGlassesRopeManwaitForElementmakes more sense.Comment #57
GrandmaGlassesRopeManComment #58
wim leerss/Grant Permissions/Grant permissions to use contextual links on blocks./
Comment #59
rachel_norfolkJust updating status to match comments...
Comment #60
GrandmaGlassesRopeMan@Wim Leers
Thanks.
Comment #62
tedbowI think it would better if you used the exact testing before and after you grant the permission.
Instead of using
$this->assertSession()->elementNotExistsYou can use
$this->assertSession()->waitForElementin the first part of the test also. waitForElement() will return NULL if not found after the wait. So you can just check that it is empty.That way we can be sure that element just doesn't exist because we have waited for it.
Comment #63
tedbowComment #64
GrandmaGlassesRopeManFixes from #62.
Comment #65
dbjpanda commentedPatch looks good. I tested it and working for me. Changing status to RTBC
Comment #67
GrandmaGlassesRopeManRandom test failure.
Comment #68
dbjpanda commentedI tested the patch and it works for me.
Comment #69
lauriiiAs mentioned on #47, this is out of scope change since successfull HTML response can never be null.
Unused use statement.
The closing brace for the class must have an empty line before it.
Comment #70
markdorisonIncorporated feedback from #69.
Comment #72
GrandmaGlassesRopeManComment #73
markdorisonComment #74
alexpottCommitted 488a6b1 and pushed to 8.4.x. Thanks!
I think we should backport this to 8.3.x - going to get a +1 from another committer because we're in RC for 8.3.0.
Comment #76
cilefen commented+1—it's a one-line clean change with test coverage to rid us of an ugly problem.
Comment #78
alexpottCommitted 600e6db and pushed to 8.3.x. Thanks!