Proposed commit message:
Issue #2351015 by plach, effulgentsia, Wim Leers, dawehner, martin107, damiankloip, cilefen, Fabianx, catch, pwolanin, Damien Tournoud, znerol, YesCT, larowlan: URL generation does not bubble cache contexts
Problem/Motivation
From #48:
Thought about this overnight, there is a security issue if we don't do this I think.
1. User A visits a page, the HTML is cached with their CSRF token.
2. User B visits the page, the User A's CSRF token is in the HTML. Clicking the link will fail.
3. However, User B then takes the link with User A's token, and creates a CSRF attack using that.
4. User A visits the page with the CSRF attack, and the link is valid for them.
CSRF token is based on session, so it'll work as long as the user has the same session ID, which could be a long time.
Proposed resolution
- Keep
\Drupal\Core\Routing\UrlGenerator
, unchanged, but make it a private service called e.g.url_generator.uncacheable
- Add
\Drupal\Core\Render\UrlGenerator
, make it the newurl_generator
service - (This is similar to what we do with
config.storage
&config.storage.active
.) - (This also happens to be exactly what Fabian and Crell concluded at #2450993-24: Rendered Cache Metadata created during the main controller request gets lost, point 6. :))
- (Also: this is a great validation of the architecture: we have a flawed URL generation API, and the fix is filthy, but we can fix it by just swapping the service out for another implementation that thinly layers on top of the existing implementation, but also is coupled to the renderer. Isolated crappiness. Hurray! :))
- Don't add new methods to
Renderer
, just use the same technique thatThemeManager
uses to allow theme preprocess functions to specify$variables['#attached']
: callRenderer::render()
with an in-place generated render array that contains the cacheability metadata. That'll put it in the renderer's context for us :) - After #2450993: Rendered Cache Metadata created during the main controller request gets lost landed, we get exceptions complaining that no render context is active, so solving all failures in the patch means fixing all tested occurrences of generating a URL (without collecting cacheability metadata) outside of an explicit render context. (i.e. they end up in the early rendering wrapping subscriber introduced by #2450993.)
- In doing the above, we found several places in core that did generate URLs outside of an explicit render context. They are all in REST (
RequestHandler
) and Views (ViewAjaxController
). - But there are also several controllers returning AJAX responses. These are basically minimally upgraded compared to Drupal 7 (compare D8's API with D7's: https://api.drupal.org/api/drupal/includes%21ajax.inc/group/ajax_commands/7). Which means the same reasoning as in #2450993 applies: be gentle with legacy-style code; ease the transition to D8. This is why we were forgiving for render arrays in #2450993, and for the same reason this issue chooses to be forgiving for AJAX responses.
- Besides that, there are also several controllers that explicitly chose to opt out from caching altogether: their routes have the
no_cache: TRUE
option.EarlyRenderingControllerWrapperSubscriber
ignores these routes. - And finally, there is some code — e.g. when sending e-mails — that runs outside of controllers and therefore cacheability is irrelevant and does not need to be bubbled. To not burden those cases, this patch detects whether a render context is active, and if there isn't, it doesn't bubble (see
CacheableMetadataAwareUrlGenerator::bubble()
). - Finally, it simply fixes a bunch of places that were incorrect:
FormElement::processAutocomplete()
,RenderElement::preRenderAjaxForm()
,TextFormat::processFormat()
,shortcut_preprocess_page()
,\Drupal\views\Plugin\views\pager\SqlBase
and\Drupal\views\Plugin\views\style\Table
.
And in implementing this, a noteworthy bug was discovered: HEAD's rendering bubbleable metadata bubbling breaks when a subrequest happens (which CommentController::commentPermalink()
uses), because HEAD doesn't tie the render context to the current request. This patch fixes that.
Remaining tasks
TBD
User interface changes
None.
API changes
- API addition:
RendererInterface::hasRenderContext()
- Behavior change: Pager links no longer include the domain/path, they are querystring-only URLs. This ensures pagers are not cacheable only for the current URL. (Think a View block with a mini pager that is now cacheable across pages, whereas in HEAD it needed to be cached separately for every URL it appeared on!).
- API addition:
#type => pager
now has an overridable#route_name
property, which defaults to<none>
. - API addition:
UrlGenerator
is now able to generate querystring-only URLs (needed for pager, see above). - API addition: theme preprocess hooks can now specify
$variables['#cache']
(needed for pager). - Behavior addition:
WebTestBase::clickLink()
now supports querystring-only URLs
Comment | File | Size | Author |
---|---|---|---|
#308 | interdiff.txt | 4.62 KB | Wim Leers |
#308 | 2351015-308.patch | 85.66 KB | Wim Leers |
#307 | interdiff.txt | 9.69 KB | Wim Leers |
#307 | 2351015-307.patch | 85.66 KB | Wim Leers |
#305 | interdiff.txt | 8.31 KB | Wim Leers |
Comments
Comment #1
Fabianx CreditAttribution: Fabianx commentedComment #2
Fabianx CreditAttribution: Fabianx commentedComment #3
catchIf you don't send the $value argument to drupal_get_token(), you already get the same token for every occurrence on the page.
So I think what you're really saying is that we'd restrict $value to a handful of constants? That seems completely reasonable. Some of the worst hacks in cacheable_csrf are due to the value argument, especially for links.
Comment #4
Wim LeersOMG AWESOME!
I'll help with this patch once you get it going — definitely by providing reviews, but if necessary also by pushing it further. I'd love to see this in :)
This will make all blocks containing forms cacheable, but also Views exposed filters!
Comment #5
catchI've read through this a few times now and it looks great!
If we use cookies/local storage, then this feels like exactly the same security model that cacheable_csrf has.
I think it'd be good to get someone less involved in 8.x caching, but with a big interest in security to look at the client-side tokens, just in case we missed something. I especially say that as the main author of cacheable_csrf, but it has had review from several other people by now already so feel pretty good about it personally.
If we use #post_render_cache then there's no change in the security model compared to 7.x - assuming we support token groups (for admin vs. non-admin etc.). We'd also need to do that for the fallback confirm forms anyway.
Comment #6
Fabianx CreditAttribution: Fabianx commentedIt seems we already have an indication that routes are _admin: TRUE, which we could well use as the default for the CSRF-TokenGroup to get the CSRF token.
I unfortunately don't have an implementation plan, yet. Who is currently working on the CSRF to service conversion?
Would that be @znerol?
And does anyone know who implemented _csrf tokens for routes?
I would like to implement it in a way that is least disturbing to current efforts in that conversion area and in agreement with the person that did the router integration.
Comment #7
catch#1798296: Integrate CSRF link token directly into routing system was the original issue. We discussed keeping the route flag but swapping out the implementation of the token to something cacheable/replaceable on the issue too.
Comment #8
znerol CreditAttribution: znerol commentedGreat plan.
For the record, the implementation in Authcache does not even bother collecting all the tokens into one request. This made the ESI implementation straight forward. The only problem on real sites I've encountered with this approach is the commerce add-to-cart form (which generates as many different forms as there are buttons on the page). But this would be easily solvable with the proposed csrf group method.
There is still the problem with cached forms on cacheable pages , but it looks like this is worked on in #2242749: Port Form API security fix SA-CORE-2014-002 to Drupal 8.
Comment #9
damiankloip CreditAttribution: damiankloip commentedThis is a rough start, but we would need something like this. Has a few benefits first off;
- Outbound route processor is greatly simplified - it just needs to insert a placeholder
- The CsrfAccessCheck would also not need to do the path dance. It would just use the _csrf_token requirement value from the route. As long as we are ok with using this limited set of values. I think it would be fine, per-path is maybe a step too far, more idealistic from a security point of view but not everything else. So security feedback would be good. Some compromise here.
Comment #10
Wim Leersdamiankloip++ :)
Comment #11
Damien Tournoud CreditAttribution: Damien Tournoud commentedAt that point, wouldn't it be a lot easier to just start using the
Origin
header instead of token-based validation?Comment #12
Fabianx CreditAttribution: Fabianx commented#11 That is a possibility, too, but I believe in micro-steps - even if that means some work will be thrown away, but if someone gets Origin approved via SecTeam, I am not gonna complain :).
Comment #13
Damien Tournoud CreditAttribution: Damien Tournoud commentedOrigin is approved by the secteam, we discussed that internally several times. The only problem with this is browser support, but I guess it's not a problem anymore for Drupal 8?
Comment #14
Damien Tournoud CreditAttribution: Damien Tournoud commented(Relying on the
Origin
header could actually be *more* secure than token-based validation because third-party plugins seem to be doing a better job at preventing theOrigin
header from being spoofed than at enforcing basic cross-domain boundaries.)Comment #15
Fabianx CreditAttribution: Fabianx commentedI think the origin header might be feasible now, but if we just implement
CSRF -> #post_render_cache
this would unblock a ton of issues without any discussion about that, so it probably makes sense to do that anyway and leave the origin discussion to another issue.
Comment #16
catchSince we're only doing PHP replacement here there's no significant change to the security model, seems OK to proceed.
I do think we should discuss the Origin header before looking at JavaScript replacement, since that's more complex to implement.
Comment #17
damiankloip CreditAttribution: damiankloip commentedAgreed. Sounds like a sensible way forward.
Comment #18
catchSince this is really the only way to fix #2335661: Outbound path & route processors must specify cacheability metadata properly, bumping to critical.
Comment #19
znerol CreditAttribution: znerol commentedIn order to reduce risk and speed up this issue, I'd rather only attack menu-link CSRF tokens here and do forms in another issue. This is because some forms may have user dependent
#default_value
and caching them will cause problems (e.g. #2307555: Comments intermittently misattributed over in Authcache was one not so obvious instance of that problem. The same fragile code still exists in D8 AFAIK). We cannot expect #4 to magically happen by just offloading CSRF form token generation to Ajax or#post_render_cache
.Comment #20
Fabianx CreditAttribution: Fabianx commentedI agree with znerol and that is the MVP to strive here:
- Convert menu CSRF tokens to use #post_render_cache
Comment #21
moshe weitzman CreditAttribution: moshe weitzman commentedWould anyone be willing to Assign this issue to themselves. I'm a Base system component maintainer and doing triage on Critical items.
Comment #22
moshe weitzman CreditAttribution: moshe weitzman commentedComment #23
damiankloip CreditAttribution: damiankloip commentedI can do this. I did the first half. Might as well carry on I guess :)
Comment #24
damiankloip CreditAttribution: damiankloip commentedNew patch with a CsrfController instead. Will talk to Fabianx about the render cache side of things. Should have something today.
Comment #25
dawehner@alexpott, @effulgentsia, @xjm, and I discussed this and were unclear as to how this cache poisoning bug would be more than a very rare edgecase, and it's not a security bug because sessions that receive the invalid link would still be denied access.
The only case the cache poisoning would occur is if a link with a CSRF token were added to a menu.
This is really unlikely given that CRSF dependend routes are dynamic.
Examples:
/comment/299792/approve
If someone puts those dynamic links into menu links, they have other problems. CSRF links appears on admin listing forms, not on usual menu links.
Comment #26
xjmIt does sound from #19 and #20 that we want a separate issue for forms, and I think that separate issue would actually be critical?
Comment #27
znerol CreditAttribution: znerol commentedNote #144538: User logout is vulnerable to CSRF. Also I think this is not only about menu, but also render cache (?)
Regarding forms: This is IMHO not feasible unless most core/contrib forms are crafted in a way that they are cacheable. This is not the case at the moment. E.g. the comment form puts the username in a
'#type' => 'value'
element, the contact form has profile data in#default_value
. Therefore in this cases it is not enough to just dynamically generate the CSRF token. Rather it is necessary to strip personalized data from the form (and perhaps restore it in the browser via Ajax).What we might want to do is adding cacheability metadata to forms as a first step (or even form fields?).
Comment #28
catchIf we make the csrf token support for forms cacheable, then it'd make it much easier to create a cacheable form at the API level (i.e. without requiring cacheable_csrf module). I think it's very unlikely we'll be able to enable HTML caching of all forms by default though for the reasons znerol states.
Comment #29
catchThis is blocking #1965074: Add cache wrapper to the UrlGenerator and #1805054: Cache localized, access filtered, URL resolved, and rendered menu trees, adding tag.
Comment #30
Fabianx CreditAttribution: Fabianx commentedSo the problem with using #post_render_cache at this moment is that the URL might be created at a moment, when the render stack is not yet available.
I thought about two ways:
a) Inform some subscriber about the used tokens, still restrict to token groups and add something to replace the tokens in markup _before_ pushing to cache. A little error prone but given we know how links look it could work as long as we have a mapping of value => token somewhere, we can replace it with the original value and replace the token dynamically. I will explore that option in Drupal 7 more.
b) Lazily create the tokens for the URLs before any __toString happens.
The advantage is that the __toString should happen in a rendering context (when we want to use render caching), while the new Url() might not be.
Soooo:
Given we now have a renderer service that is possible now, even better if URL would not return a string but a render array ...
I am using that strategy in Drupal 7 to support drupal_add_js/drupal_add_css and it works great!
Comment #31
YesCT CreditAttribution: YesCT commentedneeds the remaining tasks identified in the issue summary
Comment #32
YesCT CreditAttribution: YesCT commentedand to see if the summary can be more of a summary: be more concise
Comment #33
Wim LeersRelated reading: http://www.fastly.com/blog/Caching-the-Uncacheable-CSRF-security/
Comment #34
Anonymous (not verified) CreditAttribution: Anonymous commentedre. #33 - interesting. maybe Wim should write a blog post about 'cache blobs with tokens, and replace them within the context of single request' as another way to do this ;-)
Comment #35
dawehner.
Comment #36
cilefen CreditAttribution: cilefen commentedreroll
Comment #37
cilefen CreditAttribution: cilefen commentedoops
Comment #38
cilefen CreditAttribution: cilefen commentedThere is a patch, so "needs review".
Comment #41
martin107 CreditAttribution: martin107 commentedNothing special, just found couple of minor nits while reading up on this.
Comment #42
martin107 CreditAttribution: martin107 commentedAh 0 bytes in the last patch :(
Comment #44
martin107 CreditAttribution: martin107 commentedWhile looking at the test failings in RouteProcessorCsrfTest I can see :-
a documentation bug on the interface.
a couple of invalid assertNull's
and some testing that is too tightly coupled to the current implementation, which will need fixing for this issue to pass.
In short a bundle of side issues that now have a dedicated issue all of their own.
#2430167: Broken tests for \Drupal\Core\RouteProcessor\OutboundRouteProcessorInterface::processOutbound
Comment #45
martin107 CreditAttribution: martin107 commentedThis is me acting on #44
RouteProcessorCsrfTest has been refactored in light of the fact that RouteProcessorCsrf no longer depends on CsrfToken.
[ One failing test should now pass. ]
Comment #47
catchRetitling - this is any link.
I don't think this blocks the related issues - we already cache some links which may or may not be CSRF enabled.
Also we discussed this on the maintainer call and are thinking about downgrading to major, but this was a very borderline decision and I need to type the reasoning up properly - so leaving critical unless that's typed up.
Comment #48
catchThought about this overnight, there is a security issue if we don't do this I think.
1. User A visits a page, the HTML is cached with their CSRF token.
2. User B visits the page, the User A's CSRF token is in the HTML. Clicking the link will fail.
3. However, User B then takes the link with User A's token, and creates a CSRF attack using that.
4. User A visits the page with the CSRF attack, and the link is valid for them.
CSRF token is based on session, so it'll work as long as the user has the same session ID, which could be a long time.
Comment #49
Fabianx CreditAttribution: Fabianx commentedThere is another problem we need to fix when we cache CSRF tokens.
There is currently no kind of fallback, e.g. lets say we had CSRF on /user/logout and lets further say we had a block caching that incorrectly per role instead of per user / session (even!).
That logout link would work for one user, but not for another user and there is no way to get it to work (for the user).
I have seen this on D7 sites with advanced caching, so this is not just theoretical and caching per session() is less than ideal ...
The best solution for this is:
- Have a fallback page that shows the link again and asks the user to confirm the operation with a message that CSRF validation failed.
The difference to D7 where this problem is omnipotent with links and forms (flag module, user relationships, other ajax links, ajax forms, etc.) is that in D8 cacheability is on-by-default.
Hence the larger importance to fix this.
What is stalling this issue currently is that you can generate a link outside of a render array, but the CSRF token is generated before.
The problem - like with attachments - is that you would like to push the link via some other medium like an ajax response or such and hence #post_render_cache is not called.
This is fine for forms, but difficult for the link generator.
For e.g. #type => link this issue would be trivial in comparison, as all that logic would be coupled in the render layer, where we have all the advanced caching, etc. things.
Will discuss with Wim and catch again, maybe the first step is to make anything work and then care about the edge cases of someone wanting to display a link with CSRF out of bounds.
Comment #50
effulgentsia CreditAttribution: effulgentsia at Acquia commentedDiscussed with the branch maintainers, and they were convinced by this being a security bug, and therefore critical, per #48.
Comment #51
effulgentsia CreditAttribution: effulgentsia at Acquia commentedRecategorizing as a bug and retitling to describe the bug rather than the proposed solution.
Comment #52
effulgentsia CreditAttribution: effulgentsia at Acquia commentedComment #53
Fabianx CreditAttribution: Fabianx for Drupal Association commentedComment #54
YesCT CreditAttribution: YesCT commentedupdating issue summary #1805054-138: Cache localized, access filtered, URL resolved, and rendered menu trees said this is not blocking that anymore. (still related though)
Comment #55
pwolanin CreditAttribution: pwolanin at Acquia commentedtaking a look at this
Comment #56
pwolanin CreditAttribution: pwolanin at Acquia commentedLooking at the approach so far, I'm not seeing how it would be secure against replay attacks. maybe we need to move to a post-render approach?
Comment #57
pwolanin CreditAttribution: pwolanin at Acquia commentedper discussion with Wim Leers, postponing on #2335661: Outbound path & route processors must specify cacheability metadata
We can't implement this using post-render without more generally solving the cachability of links.
The patch as is seemed only about 30% complete, and had issues like defining the same constant in multiple places and yet not using it. e.g. I started down this path for a while:
Comment #58
Fabianx CreditAttribution: Fabianx for Acquia commentedThe other issue is independently critical, but I have an idea for this one so setting briefly active again to provide a proof-of-concept (based on an idea for #2450993: Rendered Cache Metadata created during the main controller request gets lost).
Note that this will most probably not be a clean or committable solution, but it should be one that can be used to make VRC / Smartcache green in the mean time to ensure they are not blocked until we have the clean solution.
- Introduce #pre_render_cache, but with a different meaning, which means: processing needed to be done before putting the cache entry into cache
- Set #pre_render_cache data using the token generator in the render chain with both the token value and its source value.
We'll see how that works ...
Comment #59
damiankloip CreditAttribution: damiankloip commentedYes. Sounds reasonable. The approach so far is just the token placeholder side. Not the actual rendering. Fabian has some good ideas about that :)
Comment #60
Wim LeersIf Damian is also convinced, then I'm pretty sure this is going to be awesome :)
Comment #61
pwolanin CreditAttribution: pwolanin at Acquia commentedI'm not convinced that the approach in this issue is correct at all.
Please update the issue summary to clarify if you have a new approach.
Comment #62
Wim Leers+1 to pwolanin's skepticism: this needs a much clearer explanation.
Comment #63
Fabianx CreditAttribution: Fabianx for Acquia commented"Brace yourselves, Patches are coming".
The approach here is orthogonal to #2335661: Outbound path & route processors must specify cacheability metadata anyway, because that issue sets just a max-age=0 for now.
Lets start with a quick hack and proof-of-concept.
Comment #64
Fabianx CreditAttribution: Fabianx for Acquia commentedAnd here is the quick proof-of-concept.
I didn't get to upload it yesterday:
- Inform the renderer of used CSRF tokens
- Check CSRF tokens before setting cache (as the stack is bubbled here still before cacheSet, need to move that over to the bubbleStack)
- Replace CSRF tokens before putting into the cache and add a #post_render_cache
As CSRF tokens are unique that approach works easily.
- Is that a clean approach?
Not really ...
- Does it work?
Yes, it does.
- Performance impact?
Low, even with 100s of links only those few CSRF tokens are checked in the markup with a strpos and str_replace call and only when a cacheable area is present anyway ... (e.g. #cache with keys) of which we only have 3-4 dimensions currently.
Worst case is that something creates a CSRF key early on, then all other cache sets, test for that CSRF key during the request.
Cache Gets are not affected.
- How to test:
Run:
drush scr $(pwd)/core/test_csrf.php
( currently cache_set / cache_get don't work with drush as drush does not simulate a GET request, so isMethodSafe() is always false. Not sure if that is a bug or a feature, hence we look at $build['#children'] instead.)
Note:
This is a proof-of-concept that could be used in combination with other patches to ensure CSRF tokens are not an issue.
Comment #65
webchickJust the tag fairy, carry on.
Comment #66
Fabianx CreditAttribution: Fabianx for Acquia commentedJust need to re-visit once 'Outbound ...' is in.
Likely can demote to major then.
Comment #67
dawehnerThank you fabian!
Comment #68
webchickQuick issue summary update to note what this issue's postponed on.
Comment #69
dawehnerSo #2335661: Outbound path & route processors must specify cacheability metadata is in.
As far as I understand we would be safe, in case all those CSRF cache contexts are bubbled up. For menu links this is the case after #1805054: Cache localized, access filtered, URL resolved, and rendered menu trees
but in custom code, you could still render a URL to a random route, but the probability that wrong entries are stored in cache is less likely.
Comment #70
webchickIf I'm not mistaken, this is no longer blocked, since #2335661: Outbound path & route processors must specify cacheability metadata got in.
Comment #71
dawehnerRemoving assigned field to encourage also other people to look at it.
Comment #72
catchSo #2335661: Outbound path & route processors must specify cacheability metadata at least when combined with #1805054: Cache localized, access filtered, URL resolved, and rendered menu trees should fix the security issue.
Then we have the (major) issue that the presence of a CSRF token in any rendered HTML will disable caching. In core this isn't a big deal, but it means for example flag module will disable caching for all users with access to flag links on entities or similar.
On the patch, I'd expect this to look more like #2478443: Set the 'is-active' class for anonymous users in a Response Filter instead of a #post_render_cache callback. We know there are use-cases for CSRF tokens to be replaced via either PHP or JS, and the HTML parsing would also be very similar.
Comment #73
Wim LeersHah, that is exactly what @effulgentsia and I concluded yesterday too.
But!
That will fix the security issues for links with CSRF tokens if they are part of menus, NOT the more general problem of any link.
Aha, I see. The idea that @effulgentsia and I had was to have that response filter just search the HTML for CSRF token placeholders, and replace all of them, always.
But even then, we are left with the problem of other outbound path & route processing having cacheability metadata that isn't bubbled when URLs/links are added to a render array in the typical
$this->t('Please see <a href="@url">the docs</a>", ['@url' => \Drupal::url()])
way. For example:renderer.config.required_cache_contexts
./node/1
and the alias is/foo
at time A, and is render cached, yet at time B it is changed to/bar
, then we don't invalidate it, hence the cached link (/foo
) will result in a 404. This is a general problem we've never really solved (see #2480077: Using the URL alias UI to change aliases doesn't do necessary invalidations: path aliases don't have cache tags), and which you can kind of solve by using the Global Redirect. So I don't think we need to solve that specific problem here, but the general problem (no cacheability metadata) remainsBut that's the thing here: we can mitigate the problems/risks of the 99% cases (CSRF tokens via response filter + outbound language path processor by adding an additional required cache context) very easily, but doing so does not solve the general problem. One could argue that writing additional path/route processors is so very rare that it's okay for that to be kinda painful to get to work correctly in combination with render caching, but at the same time that is icky. On the other hand, solving it properly requires quiet widespread changes, which makes me lean towards solving the 99% here and then in a non-critical follow-up work on making it *better*.
Thoughts?
Comment #74
Fabianx CreditAttribution: Fabianx for Acquia commentedYes, using always placeholders and the response filter to replace them should work.
IIRC even Ajax responses go via the response filter, so as long as it does not get somehow encoded, it should work ...
The other possibility is to placeholder it on demand ...
---
$this->t('Please see <a href="@url">the docs</a>", ['@url' => \Drupal::url()])
There is a possibility, have everything go via the renderer and force code to use renderPlain() with e.g. a closure for okay DX to render something early ...
But its a chicken-egg problem:
- If you put everything through the renderer, even current request unrelated things (email, etc.) go onto the stack, possibly get an exception
- If you do not put things through the renderer, you use metadata
Same for placeholders:
If we placeholder all CSRF tokens regardless of where they appear, we might end up with placeholders somewhere that we e.g. want to send to some other service (in which case #render_placeholder is better than FilterResponseEvent as it would be at least in renderPlain processed).
If we do not placeholder CSRF tokens we could end up with security issues on custom code ...
Comment #75
Wim LeersIndeed.
But I just thought of a case where it would break: if an e-mail containing links with CSRF tokens is being sent. Those e-mails aren't responses, so would never get processed by a response filter, so … damn :(
The key problem here is that we render URLs using the same context-unaware API for vastly different use cases: when sending e-mails or when sending HTML responses…
Comment #76
dawehnerCould we introduce some basic replacement logic subsystem, which you can either use it when you render mails (maybe have a dedicated renderapi method to do so), or when you
render things via responses?
Comment #77
Fabianx CreditAttribution: Fabianx for Acquia commented#76:
Yes, that was my idea with renderPlain() taking a closure:
It's not totally nice, but it works in all cases and properly says: This is a new rendering context, render this isolated from the rest of the page ...
Comment #78
Wim LeersBut URL rendering cannot specify cache contexts. Not without #2450993: Rendered Cache Metadata created during the main controller request gets lost at least, which I'm not sure is actually going to happen.
Comment #79
catchThere's no use-case for this though.
If you send a CSRF-protected link via e-mail, there is only a small chance that it will be opened in the same session as the one that was used to generate it. Also if the token doesn't get replaced, the worst that happens is you get a 403 - same as if your session has expired/you opened the link in a different browser/on a different device. So I don't think we have to handle that case at all.
If you have an e-mail that links to an action, it'll have to be a link to a confirm form, or it'll be a special one-off link like password reset. Either way what we do here doesn't affect that.
Comment #80
Wim LeersOf course you're right.
I'll get a patch going for this.
Comment #81
dawehnerWhat are the next steps on this issue? Is this issue somehow actionable in any form?
Comment #82
Wim LeersYeah, sorry for the silence on my part. I said in #80 I was going to work on it, but then got stuck. Attempting my rough outline.
The part I got stuck on was the bit where the CSRF outbound route processor needs to track somewhere somehow which the placeholders are that it uses, because the response subscriber will need to know those placeholders. And I don't see how we can do that.
I think we kind of need #2450993: Rendered Cache Metadata created during the main controller request gets lost for this to be resolved, and fortunately @Fabianx & @Crell came up with a good plan for that at DrupalCon Los Angeles 1.5 week ago.
Assigning to @Fabianx for feedback.
@Fabianx, can you provide a very short explanation of how we can resolve this using #2450993, and link to another comment with more details?
Comment #83
Fabianx CreditAttribution: Fabianx for Acquia commented#2450993-24: Rendered Cache Metadata created during the main controller request gets lost has all the details from Crell, but here is a TL;DR of that:
- Rendering pages is like drawing on a canvas (compare 3D libraries or [canvas] HTML element)
- Render Array concatenation is like creating a larger canvas out of several smaller canvas'
We found out after much discussion that we implicitly have a 'drawing canvas' or 'drawing context' already now with the stack.
However currently we throw some of it away (#2450993: Rendered Cache Metadata created during the main controller request gets lost) and we also have APIs that don't allow easily to store / retrieve / concat whole canvas' (cacheable render arrays with #attached and #cache metadata).
We want to make that canvas way more explicit and require it always.
We also want to allow creating a new canvas on the fly, e.g. via a closure.
As that would be quite a change we also opt-in controllers to have a default drawing context, which we merge if the controller returns a render array.
That gives us the best out of both worlds:
- No exceptions for code that early renders / does something that affects the canvas within a controller. (we track it for you)
- "Scoped" canvas for everything else, as we need to explicitly create a rendering context.
e.g. the renderer does need a new rendering context whenever it goes down a tree with #cache keys set, as we want to store that subtree isolated.
If you want to render early in isolation, you get back not only a string, but also a whole cacheable render array, which you can then stuff into another render array. (early rendering works again this way!)
==
The advantage of a scoped canvas approach is that certain data is only necessary for a subtree.
E.g. once a render context is finished we can remove all SafeMarkup that relates to building and rendering this fragment / part of the page.
Similar we don't need to keep track of CSRF tokens globally that don't apply to this rendering context.
The advantage is that we no longer have one global state, but a state that is purely determined by PHP's call stack.
===
How does that relate to CSRF tokens?
CSRF tokens need to be stored somewhere, they also need to get back to the render array for cacheability reasons.
As we currently cannot rely on the canvas / render stack being processed always, this issue is only partially solvable right now.
---
Theoretically we could add one global that we always empty when we enter a new Root call or returning from a #cache keys set entry, hence simulating what we should be doing, but I would prefer to wait for #2450993: Rendered Cache Metadata created during the main controller request gets lost as it is IMHO the only sane way to resolve this issue cleanly. I will ask catch if we want to postpone this one and make #2450993: Rendered Cache Metadata created during the main controller request gets lost a critical blocker for this one, or if we just add #2450993 as a priority without making it critical.
As a side effect we should be able to improve stack performance by at least 2-5% (I guess) as we only need to merge when we come back from a new 'render context'.
Comment #84
Fabianx CreditAttribution: Fabianx for Acquia commentedAs proposed in chat with catch, made #2450993: Rendered Cache Metadata created during the main controller request gets lost critical and demoted this one as major and postponed it on that issue.
Because #2450993: Rendered Cache Metadata created during the main controller request gets lost will solve it, but this issue should verify and likely use some placeholders for CSRF tokens after that issue has been done.
Comment #85
effulgentsia CreditAttribution: effulgentsia at Acquia commented+1 to #2450993: Rendered Cache Metadata created during the main controller request gets lost being very valuable to fix regardless of this issue. As to whether or not it should be critical, I'm not sure yet. I'm curious if this patch passes tests, and if so, whether something like it would be enough to then make the rest of this issue and that one both be not critical.
Comment #87
effulgentsia CreditAttribution: effulgentsia at Acquia commentedOk, that's not looking promising. How about this?
Comment #89
webchickAssume that was a copy/pasta error.
Comment #90
effulgentsia CreditAttribution: effulgentsia at Acquia commentedComment #91
effulgentsia CreditAttribution: effulgentsia at Acquia commented#90 is green, but needs new tests to prove it fixes the bug. @Fabianx: unassigning you in case someone else wants to work on those tests or otherwise comment on #90, but re-assign to yourself if you want to.
Raising priority to critical, because so far, I'm still unconvinced about #2450993: Rendered Cache Metadata created during the main controller request gets lost remaining critical, and if that ends up getting downgraded, this specific bug is still critical.
Now, #90 is definitely ugly, and could result in false positives (e.g.,
token
parameters for something other than CSRF tokens resulting in not caching things that could be), but I'm curious what people think about this as an interim solution, and then removing it once there's a better fix, such as #2450993: Rendered Cache Metadata created during the main controller request gets lost, whether that happens before 8.0.0 or after. Despite this still needing tests, leaving at "Needs review" to gather that feedback.Comment #92
Fabianx CreditAttribution: Fabianx for Acquia commented#91: It is also potentially performance problematic to check all things again and again for tokens, however I agree its way better than doing nothing.
What about changing csrf-tokens to be prefixed with ?token=csrf-[hash] for now, that is an internal detail anyway, but it would reduce false-positives to 0.
Thanks for unassigning, yes lets have someone fix the test coverage for it.
Comment #93
dawehnerIts odd in the beginning that we don't use token-csrf, given that it would semantically describe what its used for.
Comment #94
Wim LeersWhile working on this, I ended up creating #2512132: Make CSRF links cacheable as a separate issue, because it only addresses the "CSRF links break cacheability" part, not the security part, which is why this particular issue is critical.
Comment #95
Wim LeersWith #94 splitting off cacheability, we can get better focus in this issue. Also updated the IS. So far, we've been mixing the cacheability discussion with the security discussion. So let's restate what the problems are:
\Drupal::url()
,\Drupal::l()
,Url::toString()
,UrlGenerator::generateFromRoute()
, etc. without setting the optional$collect_cacheability_metadata
parameter to TRUE, then we don't get\Drupal\Core\Access\RouteProcessorCsrf
'smax-age = 0
, and hence we end up caching the individual fragment of the page that this URL ends up in across users.Problem 2 is the part that #2450993: Rendered Cache Metadata created during the main controller request gets lost fixes. Yay!
But that still leaves us with point 1, which is a quite nasty problem as well. The reason that parameter is optional is simple: we didn't want to break BC for URL generation.
As far as I can tell, we have only two options for solving problem 1:
RendererInterface::updateRenderContext(BubbleableMetadata)
. This violates quite a lot of principles, because it'll mean tying coupling the URL generation system with the render system.P.S.: if the many ways to generate URLs make you sad, see #2491981: There are too many ways to generate URLs and links.
Also completely redid the IS, since it was mostly discussing cacheability, which is now handled by #2512132: Make CSRF links cacheable. Also migrated the relevant parts of the IS from here to there.
Comment #96
dawehnerYeah mh, that sounds like a gigantic patch and tons of broken contrib for well, a case, which is in the majority not broken ... as the link just depends things which are part of the cache context anyway (remember, most URLs don't have CSRF checks, i mean you could pretty much limit those to entity operations and similar, places which are kinda controlled).
In case we really want to do that, I would vote for changing GeneratedUr to implement
Psr\Http\Message\UriInterface
which then potentially makes us compatible again with over code out there.
Given that I fear that option 2) is kinda the more pratical one.
Comment #97
Wim LeersJust had a call about this (#95) with effulgentsia.
He had an idea to make proposal 2 not disruptive at all, which we discussed and together ended up on this proposal:
\Drupal\Core\Routing\UrlGenerator
, unchanged, but make it a private service called e.g.url_generator.uncacheable
\Drupal\Core\Render\UrlGenerator
, make it the newurl_generator
serviceconfig.storage
&config.storage.active
.)Renderer
, just use the same technique thatThemeManager
uses to allow theme preprocess functions to specify$variables['#attached']
: callRenderer::render()
with an in-place generated render array that contains the cacheability metadata. That'll put it in the renderer's context for us :)url_generator.uncacheable
— which is also more optimal for those use cases, given they never care about caching anyway.Did I capture that well, effulgentsia?
Comment #98
Fabianx CreditAttribution: Fabianx for Acquia commented+1 to #97, sounds excellent. I love that proposal ...
Comment #99
dawehnerNice!
Comment #100
Wim LeersUnassigning in case somebody wants to take this on, I thought I was going to be able to start working on this today, but that won't happen. I'll probably start working on it on Monday or Tuesday.
Comment #101
Wim LeersGiven #97 was +1'd by both Fabian & dawehner, I think we have enough consensus to pursue this solution. Updated the IS.
Comment #102
catchIsolated crappiness++
Comment #103
Wim LeersWorking on this now.
Comment #104
Wim LeersThis implements #97.
Let's see what breaks.
Comment #106
dawehnerDo we never want to allow to call it? Just curious
Doesn't bubbles this then two times, once in generateFromRoute and once in generate()?
Comment #107
Wim Leers(Working on fixing the failures and addressing #106.)
Note that the failures in #104 reflect the cache contexts that are missing in HEAD, where our tests have the wrong expectations.
The first step is to make this patch green. Then this patch will be blocked on #2450993: Rendered Cache Metadata created during the main controller request gets lost. Once that lands, we'll need to re-test this patch, and more wrong test expectations (i.e. missing cache contexts) will be revealed.
In other words: by fixing the root cause, we're finding where our expectations have been wrong, and where we've overlooked the cacheability of links. This all yields quite interesting information (as you will see in my next comment, stay tuned!).
Comment #108
Wim LeersWhile working on this, I found that a form's
action
attribute also uses a URL that is dynamically generated using<current>
, hence making that per-route, and therefore effectively any form that doesn't really care about the current page ends up being cached for every page (well, route). See #2504115-2: AJAX forms should submit to $form['#action'] instead of <current>.Comment #109
dawehnerI'm curious whether the form #action should be then interested using some form of placeholder?
Comment #110
Wim Leers#109: that's exactly what I proposed there, see #2504115-2: AJAX forms should submit to $form['#action'] instead of <current> :)
Comment #111
Wim LeersThis is turning out to be yet another treasure trove of hidden cacheability bugs!
CommentRssTest
: newurl.site
cache contexturl.site
.PageCacheTagsIntegrationTest
: newroute
cache contextroute.*
routes have been optimized to justroute
now that thanks to this patch something is adding theroute
cache context.That something is the
UserLoginBlock
, which is callinggetDestinationArray()
, which callsgenerateFromRoute('<current>', …)
, which explains it.ShortcutCacheTagsTest
+ToolbarCacheContextsTest
: newroute
cache contextshortcut_preprocess_page()
is calling$query += \Drupal::destination()->getAsArray();
, which callsgenerateFromRoute('<current>', …)
, which explains it. Except that that code should never have been executed, since it actually calls that regardless of whether it will be used. By simply moving that call into the place where it is actually being used.FrontPageTest
): newurl.site
cache contexturl.site
from\Drupal\views\Plugin\views\display\Feed::attachTo()
which builds an URL.FrontPageTest
,PagerTest
,ExposedFormTest
): ne wroute
cache contextroute
fromtemplate_preprocess_pager()
, which calls\Drupal::url('<current>')
. I thought about changing<current>
to<none>
, because this really only needs a query string, not the underlying route. But<none>
is only designed for fragment URLs, so, no luck.But, it's easy enough to change it to
($options['query']) ? '?' . UrlHelper::buildQuery($options['query']) : ''
— which hence makes theroute
cache context disappear. Which is a good thing; for example, otherwise any non-page View reused on many pages would need to be re-rendered for every single route it appears on.FieldWebTest
,GlossaryTest
): newroute
cache context.So, still expect the failures in
FieldWebTest
,GlossaryTest
. If I didn't make mistakes, that should bring us down to 6 failures.Comment #113
dawehnerAs said on IRC, this would be just wrong. You need to render all existing query parameters as well, to have something working.
Comment #114
Wim LeersThat's actually already working:
That
pager_query_add_page()
calls adds to the existing query args.BUT you're absolutely right that the corresponding cache context (
url.query_args
) is missing. So that was yet another hidden cacheability bug! Fixed.This interdiff beautifully shows how problematic the very legacy implementation of the Pager component really is.
Comment #115
Wim LeersSuch noob.
Comment #117
BerdirReminder: There's a @todo in UserLoginForm to enable caching for that once this landed.
Also, #2375695: Condition plugins should provide cache contexts AND cacheability metadata needs to be exposed will conflict with this on the page cache integration test.
Comment #118
plach@Wim:
Working a bit on this, feel free to claim it back any time.
Comment #119
plachNot yet, sorry
Comment #120
dawehnerI'm trying to understand some of the test failure ... for now this is a reroll in order to get it working locally.
Comment #122
dawehnerThat was doable.
Comment #123
plachMmh
Comment #124
dawehnerOh well, the actual patch does not have that any longer :)
Comment #126
plachOn this for reals
Comment #127
lauriiiJust a small nit but there is one extra T
Comment #128
dawehnerWe clearly need to discuss whether changing this is right always. Are there usecases to embed Drupal HTML into other sites for example?
We could also replace the implementation of ?
Let's not use the global function
Well, its nat that the URL generator is cacheable, its rather a CacheableMetadataAwareUrlGenerator, right? We used to have done A LOT of work on the cacheable URL generator, which did some actual caching. This horrible that this was blocked
I still don't get why we need to bubble twice
Comment #129
Wim Leers#128
Yes, but since #2514136: Add default clickjacking defense to core you'll have to do extra work for that already anyway, so yes, overriding the default implementation would do the trick.
\Drupal::service('renderer')->render()
but instead:
Comment #130
dawehnerWhat I think we should do is to use and fix url generator to also take query strings into account, not just fragment
Comment #131
Wim Leers+1, but that can be a major follow-up I think.
Comment #132
plachThis just fixes some test failures, reviews above still to be addressed.
Comment #133
plach.
Comment #135
Fabianx CreditAttribution: Fabianx for Acquia commentedThis is a _really_ nice usage of the decorator pattern here.
I am not sure we need to do that when the caller collects cacheable_metadata.
It won't hurt, but it is also not necessary.
@Wim: Thoughts?
That is just too bad, I will read up where this change was discussed.
Comment #136
plachMore test fixes
Comment #137
Wim Leers#135.2: that's what I said in #129.4 already, so yes :)
Comment #139
dawehnerNice progress!
The rendered link needs to play well with any other query parameter used on the page, like pager and exposed filter.
Cheater!
Let's skip this change ...
+1!!!
Comment #141
Wim Leers#2450993: Rendered Cache Metadata created during the main controller request gets lost just landed, which is why plach re-tested in #140. This is now completely unblocked :)
Comment #142
dawehnerJust reupload the patch and let's see what fails now.
Comment #145
plachLet's get rid of a few failures
Comment #147
Wim LeersLooks like this didn't actually help resolve the ~1000
Drupal\simpletest\WebTestBase->buildUrl('user/login', Array)
-style exceptions.I wonder why.
Comment #148
plachJust a reroll for now
Comment #150
plachFixed some failures
Comment #151
plach#2491981: There are too many ways to generate URLs and links is really relevant to this: we are wondering whether it should become a prerequisite. Stay tuned.
Comment #153
plachFixed PHP unit and addressed part of #128.
Comment #155
plachThis patch introduces a way to detect whether we are in a render context, which allows the URL generator to bubble cacheable metadata only when it's the case. This should fix quite some failures.
Comment #157
plachAddressed the remaining points of the review above and fixed more failures.
Comment #158
Fabianx CreditAttribution: Fabianx for Acquia commentedThere is no need to remove that.
If I collect cacheable metadata myself I probably have reasons for doing so.
Comment #160
Fabianx CreditAttribution: Fabianx for Acquia commentedThat would be nice, but is unfortunately the wrong fix.
A render context will always be empty when a controller gets one.
Comment #161
plachYep, thanks, I had reverted that part unintentionally: #157 already re-included that.
I suspected that, but I wanted to see what would happen. I'm going to figure out what's going on for the last failures tomorrow with @Wim.
The attached patch should fix the last failure, aside from those in https://qa.drupal.org/pifr/test/1088008.
Comment #162
Wim Leers+1
s/#route/#route_name/, because "route" means "route name + parameters".
Comment #163
dawehnerIt is great that its green
As wim was saying, let's use #route_name. ... Do we have any place to document that? Maybe on
\Drupal\Core\Render\Element\Pager
would actually make sense.I think we should rather put
$variables['#cache']['contexts'][] = 'url.query_args';
intotemplate_preprocess_page()
andtemplate_preprocess_mini_pager()
.Seems entirely out of scope ...
See earlier comment, we should explain more and its not that the URL generator is cacheable.
I still think its wrong to bubble twice
It should at least explain that this is rather @internal
With the change to getAbsoluteUrl, I think this entire part is not needed.
Can't we reuse getAbsoluteUrl?
Opened up a follow up for this piece of shit: #2527848: Try to get rid of Url::fromRoute in system_js_settings_alter()
Comment #164
Wim Leers+1
The code you cited specifically prevents double bubbling.
+1
RE: follow-up-to-remove-that-piece-of-shit: +1000
Comment #165
catchRe-titling.
Comment #166
plachThe comment-related test failures were caused by a sub-request being fired: @dawehner suggested to have a separate "global" render context for each request, which is what this patch is implementing. Let's see how many failures remain after reverting the wrong fix mentioned in #160. Reviews still to be addressed.
Comment #168
Wim LeersYep, so a render context per request; to allow subrequests to have entirely separate render contexts. Makes total sense. Before #2450993: Rendered Cache Metadata created during the main controller request gets lost + this patch, that bug was simply being masked. It's been there all along.
(If a service contains state, like the Renderer does, then it needs to ensure it is different per request.)
The interdiff looks great! Basically only nits. Curious how many failures we'll have left after this :)
It's not an array, but an
\SplObjectStorage
.The patch looks strange because you moved this to the top.
Can we move it to where
protected static $context;
used to be defined? That'd make the patch much more legible.Besides, we always list the injected services first, then the other protected class properties.
Nice :)
When can this happen? I don't understand this bit.
Incomplete docblock.
Also, why is
NULL
an acceptable variable? The docblock should say so.Nit: a bunch of indentation problems. PHPStorm does this sometimes :(
Comment #169
plachThis should fix a few more failures by adding the UncacheableUrl class and using to fix REST entity canonical URIs.
Reviews not addressed yet, sorry, focusing on test failures for now.
Edit: Aw, I forgot some crap in PageCacheTest, I'll fix it in the next patch.
Comment #170
plachMore fixes
Comment #173
plachHopefully more
Comment #175
plachMOAR fixes
Comment #176
dawehnerAdded a follow up to improve the DX: #2528598: Opt out of cacheablity protection on route level
Comment #178
plachAnd a few more
Comment #179
dawehnerSome cleanup in the meantime.
Comment #181
plachReuploading #178.
Comment #184
larowlanThis patch really highlights the dark corners of core. Batch API. Pager. Updates. Places we just didn't get time to modernize.
Is this explicitly relying on the early rendering catch-all?
Do we have a plan to fix this? Or is it just to find bits in core (like pager, batch api etc) that aren't doing things correctly?
Is there merit in injecting the uncacheable url generator and setting it on the uncacheable url too - to avoid container calls? Or are these in such minority that not worth it?
Can this bleed into subsequent tests? Should we be marking this test as requiring an isolated process like we do for functional and plan to do for kernel tests?
Comment #185
plachThis includes also the interdiff from #179.
@larowlan:
Thanks for the review, I will address it soon :)
Well, not only them: also every controller dealing with non-HTML responses has issues. We'll have to create a follow-up to polish the way "rendering" (i.e. stringification) is applied to non-HTML stuff.
Not sure actually, but in any case the response will be a render array so the metadata will be eventually bubbled.
Just debugging crap, not a good moment to review this code ;)
We'd have to profile but the amount of uncacheable URLs should be fairly small.
Comment #186
plachComment #188
plachAnd more fixes, this might come back green. Addressing reviews now.
Comment #189
plachWrong interdiff, sorry.
Comment #190
plachThis should address #163 and #168. Working on test coverage now.
@larowlan:
I checked the PHPUnit docs and that should not be necessary:
See https://phpunit.de/manual/current/en/fixtures.html#fixtures.global-state.
Comment #191
Wim LeersCan't we avoid changing
_batch_page()
?Looking at its callers:
BatchController
already is executed in a render context because it's a regular controller.DbUpdateController
already is executed in a render context because it's a regular controllerauthorize.php
would need to execute it in a render context indeedinstall.core.inc
would need to execute it in a render context indeedI guess it makes kinda sense then, it could go either way.
But this is *so* hard to review, so I'm assuming you purely indented and wrapped things.
Changes here can be reverted.
Naming mismatch.
We should document why we only call this if there actually is a render context.
Let's document why it's okay to use an uncacheable URL here.
AFAICT this is NOT uncacheable, because
$path
is assigned to a data- attribute that makes it way into the form.Also:
$access
's cacheability metadata should also be applied. Technically out of scope here though.For similar reasons, this is not actually cacheable. This ends up in
drupalSettings
, and ifdrupalSettings
varies per route, we need to know that.But, given that
drupalSettings
already vary per route (seesystem_js_settings_alter()
), this would be okay to stay.This either needs docs explaining why this is okay, or it could just apply the cacheability metadata of the URL to
$element
, that's probably less work.Please see #168.2.
Hrm, doesn't this mean you can still do
->setUrlGenerator(\Drupal::service('url_generator'));
?Though I guess we can't protect against that.
I'm wondering if
Url::hasUrlGenerator()
would be better?I don't think this is correct.
Also, why not just change this to:
It seems this patch uses
UncacheableUrl
in admin-ish places. This here is clearly an admin route. The text format one (previous comment) is admin-ish, but can also appear on other places.What about just doing #2528598: Opt out of cacheablity protection on route level and getting rid of
UncacheableUrl
? That'd keep things much simpler!That would also solve this.
Why these changes?
Interesting :)
I don't understand these changes; this is a controller that doesn't return responses other than
RedirectResponse
(for which exceptions are never thrown), isn't this already wrapped in a render context?Comment #192
Wim LeersReview of #190's interdiff.
This is just for debugging the patch, right?
EDIT: oh, I guess it's to make it clear while debugging code for the remainder of D8 cycle. That makes some sense. Semantics++
OTOH, the PHP call stack already shows that anyway. So I'm not sure if it's actually very valuable.
I could live with this I think, curious what Fabian/catch/dawehner think.
Comment #194
dawehnerIn that case I agree, opt out of batch on the routing level is fine.
Given that this ends up on GET requests, we actually need to apply the cacheablity metadata, so let's do it correctly and apply the metadata
to the element.
MH, we should actually. We should override set and check for the additional interface. Plach was suggesting
Wlel, I think we should give a shit about filter tips, dont' we?
Continue later
Comment #195
plachThis should address the reviews above. A few replies to #191:
1: Implemented the fix suggested in #2528598-2: Opt out of cacheablity protection on route level and reverted the related changes.
2: Already done earlier
3: Also this one should be already fixed.
4: Done
5: Fixed
6: Fixed
7: I had moved the definition of the static property below the other ones, as before it was surrounded by non-static properties which was very confusing.
8: I discussed with @dawehner a way for the cacheable metadata aware decorator to identify uncacheable URLs and skip bubbling for them.
9: We are not checking whether the generator is set, HEAD does that unconditionally, we are just avoiding collecting metadata when the URL is uncacheable. Removed this in favor of #8.
10: Fixed.
11: Both @dawehner and I find the
UncacheableUrl
pattern very handy, we don't think getting rid of it will improve DX.12: We don't want to disable caching for REST routes AFAICT, so in this case an uncacheable URL is the way to go IMHO, until we have a better handling of non-HTML routes.
13: The test was erroneously relying on the
<none>
route to generate a language-rewritten base url, relying on an implementation detail, because when the outbound language processor sets its own base url (domain-based language negotiation) the remaining URL processing is skipped.15: Not sure which changes you were referring to, however any change to
DbUpdateController
is reverted.@#192:
I got rid of all that debugging stuff, we can always get it in later if needed.
Comment #197
plachRerolled
Comment #199
Wim Leers#195.7: ok.
#195.10: that solution works as well :)
#195.11/12: hrm, I'm not entirely convinced yet. But let's go for it now. Let's see what other reviewers think about
UncacheableUrl
:)#195.13: oh, wow :P
EDIT: oh, and thanks for addressing everything else!
So now it's moved *back* here. Why?
The first overwrites any pre-existing
#cache
metadata, and the second overwrites the first.Should be:
Similar overwriting problem.
Comment #200
plach#199.1 still to be addressed, I want to try it in isolation.
Comment #201
Wim Leers$access
is not guaranteed to implementCacheableDependencyInterface
, so it needs to be wrapped inCacheableMetadata::createFromObject()
.Comment #202
plachLast stuff, on tests now. Hopefully I didn't introduce too many failures...
Comment #204
Wim Leers#202's interdiff looks great!
Just one remark:
Why?
Comment #205
plachit bugged me, but I can revert it :)
Comment #208
plachHere is some test coverage for the main issue plus some minor code clean-up, working on some unit tests now.
Comment #209
dawehnerDo yeah we should add test coverage for that.
Do we mind putting an @see to the UncachedUrl to make that more discoverage?
I think its totally fine to create the object here. Passing that in is kinda like passing in an array, as its a "dependency".
I think we should interact with the variable as if it would not static, so we might be able really easy to switch over in the future.
Do you mind open a follow to change the drupal_render call?
What about UncacheableUrl::fromUrl()
The second drupalGet() seems pointless, let's just use $this->getAbsoluteUrl()
Comment #213
Fabianx CreditAttribution: Fabianx for Acquia commentedI think we should ask for the page_cache_kill_switch service here and not special case routes?
Hmmm, that won't work, but does that mean that when page_cache module is not loaded, that the no cache routes are cached by reverse proxies and potentially browsers???
Uhm, will follow-up with Wim. @WimLeers: ^^
Should add a @todo when that other issue is created.
That sentence needs a little work:
- e.g. s/affected/affect/
Can we check if generatedUrl->isEmpty() / isDefault() / isChanged()?
Probably not helpful, because url.site will always be present.
We need to always merge $access into $element.
The information that access was denied also needs to be added.
That is an unrelated bug though, but would be nice to fix here, as else it "looks" correct (deals with cacheability), but is not.
More concreate:
I propose to move this down one level and just have one $cacheable_metadata variable that is first assigned to $access, then if access is granted, update it with the generatedUrl.
Should we not use isset()?
Or is the try catch for the $request object?
In that case I think its okay to have a NULL request be its own state - in case it exists?
---
The below confirms, isset() should work.
s/to concern/to be concerned
s/#cached/#cache/
Lets blacklist '#cache' 'keys' here, that would have very interesting consequences ...
Still makes me a little uneasy, but way better than having url_generator.uncacheable be public, so
reluctant +1
given that Url:: should be our only API.
I don't think we need this any longer with the UNCACHEABLE_OPTION.
I would prefer it that service was private by default - if possible.
Oh, wow ... I assume this is still necessary with the Renderer changes?
Nice!
This uses uncacheable Url because this is just descriptive permissions callback text, okay got it.
I think it would be okay to rather allow:
$generated_url = $format->url(TRUE);
@url => $generated_url->getGeneratedUrl();
Plus a comment that generated cacheable metadata is discarded.
On the other hand UncacheableUrl is much more explicit.
This should no longer be needed now we check for hasRenderContext()
Not opposed to it, but is it still needed with the hasRenderContext()?
Huhhh,
Edge Case:
We will need to check if $preview correctly bubbles the metadata to the response.
That is AjaxHandler and unrelated to this issue, but worth noting here.
@WimLeers: ^^
We will carefully need to document the difference.
That DX still feels questionable.
Comment #214
Wim LeersSo
\Drupal\Core\PageCache\ResponsePolicy\DenyNoCacheRoutes
is always used, because\Drupal\Core\EventSubscriber\FinishResponseSubscriber
calls it, independently of the page cache module. Its logic:Hence on
no_cache: TRUE
routes,$is_cacheable
is always false, hence the latter branch is always entered, hence we ensure reverse proxies/CDNs don't cache them.Comment #215
plachImproved test coverage and addressed reviews. Replies coming soon.
Comment #216
plach@#209:
3: I don't get whether that's supposed to be a +1
4: Storm complains about that
@#213:
Great review :)
1: Not qualified to answer about this, but I thought
'no_cache'
would affect also logged users3: No such methods on
GeneratedUrl
, I kept the original code8: Hopefully I got it right :)
9: I agree
Url
should be our only API, however atm we have this "hybrid" cases where we have controllers doing early rendering and then not returning render arrays. This happens mostly with non HTML responses (mainly AJAX and REST). I really think we should mimic the HTML rendering pipeline and have non-HTML renderers: this would allow us to return data models similar to the render array and have,HalRenderer
orAjaxRenderer
kick-in and generate the response content. This should make dealing with cacheability of non-HTML responses way easier and more consistent with the HTML responses. We need a follow-up to discuss this stuff. At that point we could easily deprecate theUncacheableUrl
object and remove any occurrence from core.11: Well, it's necessary because code run in a controller always has a render context and REST code return resource responses...
13: I kept it as is, explicitness++
14: Not entirely sure about that due to #11, posted two patches to compare the test results
15: Yep, due to #11: updated/batch code run inside a controller but does return render arrays
18: Hopefully now it's better
Comment #219
plachAw
Comment #220
plachLet's try again without replacing the URL generator for tests
Comment #221
Wim Leers?
Yay :)
Yay :)
AFAICT you can simplify this to:
But whichever @dawehner prefers is fine.
These don't need the
->setAbsolute()
call before being passed todrupalGet()
.I think they're being set to absolute for the assertion after the
drupalGet()
?This is confusing.
No need to define a new controller; this can just reuse the
CacheTestController::urlBubbling()
controller. It's the additional route option that matters.I don't get the "Btw, actually this is not the case as the URL is absolute." part of the string.
Contradiction.
Comment #222
Fabianx CreditAttribution: Fabianx for Acquia commentedWow, why is that necessary?
Should use $preprocess_bubbleable here instead of $variables ...
+100
Comment #223
dawehnerMh, I think we should avoid processing power, if possible and so go with the route, if available.
Does someone mind to explain why?
Comment #224
plachThis adds unit testing, addresses #221 and #213.14.
Hopefully it should be complete. Reviews welcome :)
Comment #225
Wim LeersIndeed; that was my thinking too. I just wanted to point out that it could be simplified.
See #222.2 for how this needs to be fixed/improved. But then we'll still be unsetting cache keys. Why? Because this is not rendering anything, hence also not render caching anything, hence cache keys don't make sense. So we just unset them if present. This is solely about bubbling cacheability metadata.
Comment #226
plachAddressed #222.2, nice catch!
Comment #227
plachThe interdiff...
Because otherwise returning an HTML response without attachments will break, see CacheTestController.
Comment #228
plach/me needs some rest
Comment #231
Wim LeersI did one final complete review. This patch looks ready to be RTBC'd to me. Only small stuff. One bit of extra test coverage, and besides that, basically only nitpicks. And observations of particularly excellent parts of the patch :)
Nit:
Nit: unnecessary change.
I think this means we also want to update
\Drupal\system\Tests\Common\EarlyRenderingControllerTest::testEarlyRendering()
.For the ones where we do an
assertResponse(500)
, duplicate their routes, suffix their route names with_no_cache
and just add theno_cache: TRUE
route option.I think this looks great now. It represents the best possible compromise of strictness and DX, thanks to its automatic detecting of a
RenderContext
being present, andUncacheableUrl
.It's unfortunate complexity, but it's isolated crappiness (see #97 + #10). Without huge additional amounts of refactoring in the URL generation APIs, this feels like a great compromise.
(For those wanting to help make things better, please help out in #2491981: There are too many ways to generate URLs and links.)
Explained in #227 and confirmed in manual testing.
I first wanted to fix this in what seemed the proper place:
HtmlResponseAttachmentsProcessor
.but then I noticed that in fact we still had a failure, because
HtmlResponse::setContent()
always calls$this->setAttachments($content['#attached']);
.So, I think this change is actually fine.
Great call! And thanks for documenting these!
Same as first point.
I'm pretty sure this is the best documented unit test method!
plach++
And likewise, this is is extremely clear. Wonderful!
Nit: s/readonly/read-only/
Comment #232
effulgentsia CreditAttribution: effulgentsia at Acquia commentedThe if statement means we don't bubble when we're outside rendering context, so I don't understand why the UncacheableUrl docs is telling me to use it outside of rendering context. Maybe the next question will help clarify.
Why are we using UncacheableUrl in these two places?
Comment #234
plachAddressed #232 and fixed the
CacheableMetadataAwareUrlGeneratorTest
definition.@effulgentsia:
::hasRenderContext()
will return TRUE if we are inside any render context, including the early one. The comment is talking about the root rendering context, which means uncacheable URLs should be used only in the early render context (or outside controllers).The comment controller initiates a subrequest (
comment/3
->node/1#comment-3
), thus all cacheable metadata are already collected during that one. The idea is that we don't need to bubble cacheability metadata in the main request.The View edit form is an administrative route and we don't care about caching there.
Comment #235
plachBtw, it seems
UncaughtExceptionTest
is failing randomly...Comment #238
plachComment #239
plachDone for tonight
Comment #240
effulgentsia CreditAttribution: effulgentsia at Acquia commentedI still don't get it, so opened #2529714: Test helper for 2351015, which reverts those two hunks, to see if I can understand from the tests that fail.
Comment #241
effulgentsia CreditAttribution: effulgentsia at Acquia commentedHere's in-progress work on removing all usages of UncachableUrl. I suggest removing the class entirely.
Comment #242
effulgentsia CreditAttribution: effulgentsia at Acquia commentedI don't know what the original reasoning was for throwing an exception rather than automatically adding dependencies to CacheableResponseInterface objects returned by controllers, but I think this issue provides justification to rethink that and do the automatic dependency addition, per above hunk, since CacheableResponseInterface provides the interface to do so, so why not use it. REST and EntityNormalizer are a great example for needing it: REST controllers should be able to return cacheable response objects, but not have to themselves care about normalizers outputting URLs that require special caching considerations.
Comment #243
effulgentsia CreditAttribution: effulgentsia at Acquia commentedNote that some of the usages of UncacheableUrl in #234 were incorrect. EntityNormalizer results in the URL being output in the body of a cacheable response. Even for comment permalink, the subrequest could have logic that depends on the URL (e.g., block visibility conditions), so if the generated URL changes, the permalink's cached response needs to be invalidated. That's why I don't think we should provide the class: it has few if any legitimate use cases, and way too easy to use incorrectly.
Comment #244
Wim LeersStrong -1. That's exactly what we avoided in #2450993: Rendered Cache Metadata created during the main controller request gets lost! Please read the documentation on
EarlyRenderingControllerWrapperSubscriber
for the reasoning. For more detail, see the mentioned issue, specifically #2450993-114: Rendered Cache Metadata created during the main controller request gets lost (which has a detailed explanation + a table), and #2450993-123: Rendered Cache Metadata created during the main controller request gets lost & subsequent comments for more nuance.Comment #246
dawehnerWell, what means incorrect here. The path to an entity does not change, nor will ever contain CSRF tokens, so UncachedUrl is primarily a developer tool, a tool
to not scare people off.
Comment #247
effulgentsia CreditAttribution: effulgentsia at Acquia commentedThe URL can. Who knows what crazy, dynamic outbound path processors people install.
Comment #248
dawehnerNow we are overthinking, IMHO.
Comment #249
plachWell, if there's a way to get URL strings during early rendering without discarding their cacheable metadata and still return a non-HTML response, I'm fine with removing
UncacheableUrl
. However non-HTML responses do not handle early render cacheability metadata automatically as HTML responses do, so IMHO the real fix is a better handling of non-HTML responses. TheUncacheableUrl
class is a way to deal with edge cases in these contexts, for URLs that are very unlikely to change, until a proper fix is implemented. At that point we can safely deprecate it and remove every usage from core. See also #216.9.Comment #250
effulgentsia CreditAttribution: effulgentsia at Acquia commentedSo, #244 says that we want to maintain the strictness of the early rendering subscriber to throw an exception for any early rendering done by a controller that doesn't return a render array.
But, #248 says that generating a URL is not really early rendering, especially for the URLs that don't need to bubble anything, such as entity URLs, if people do what is sane and don't implement volatile path processors on entity URLs.
So, here's an approach that reverts the subscriber change, but changes CacheableMetadataAwareUrlGenerator::bubble() to only ->render() when there is something that needs to bubble. I discussed this with Wim, and he wasn't completely happy with it, because it means you might have a working site until you enable a certain path processing module, and then start getting exceptions with that module enabled. But, I think we can have non-critical followups to try to improve on that. I don't think that's a completely unacceptable situation: we can tell people that yes, volatile path processors are incompatible with controllers that render URLs directly into a Response object. There are numerous ways that contrib could work around such a conflict, should it occur in practice.
Comment #252
effulgentsia CreditAttribution: effulgentsia at Acquia commentedOk, easing up on the exception throwing when not in a render context.
Comment #256
effulgentsia CreditAttribution: effulgentsia at Acquia commentedI changed my mind on a couple things and want to try something different.
Comment #257
effulgentsia CreditAttribution: effulgentsia at Acquia commentedOk, bear with me. I'm going to do this in a few steps. First, starting over at #234, so reuploading that for clarity.
Comment #258
effulgentsia CreditAttribution: effulgentsia at Acquia commentedThis reverts/removes all traces of UncacheableUrl, but doesn't change anything else, so will fail some tests. Will soon post fixes to that.
Comment #260
effulgentsia CreditAttribution: effulgentsia at Acquia commentedThis adds AjaxResponse as a type of controller result that EarlyRenderingControllerWrapperSubscriber can autofix. I discussed that with Wim and he was okay with that, given that AjaxResponse is just an OOified version of a D7 concept, and one that a decent amount of contrib modules use, and it involves a fair amount of rendering. I think it's important to do this, because telling contrib that they can't call url() within a controller that returns an AjaxResponse seems pretty disruptive.
I'm ok though with Wim's insistence that we keep being strict about controllers that return other kinds of Response objects not doing any early rendering, including with this issue, calling url() (without collecting their own cacheability metadata). In core, this only seems to affect the Comment Permalink and REST controllers, so this patch fixes those two to collect and use the cache metadata.
In Drupal, we don't have any URLs that are very unlikely to change, because even seemingly stable ones, like entity URLs, still run through the language outbound processor which adds some cache metadata. And so if REST responses include those, then the corresponding cache contexts and tags from language negotiation need to get added to the response as well. I think we have to accept that generating any Drupal URL carries with it cache metadata that we shouldn't discard, but I think this patch now handles that pretty well.
Comment #262
dawehnerI'm curious, why does getAttachments not always return an empty array? This if seems to be a bit redundant.
Comment #263
Wim Leers+1, for the reasons stated in #260.
But I find the first if-test (the one for
CacheableResponseInterface
) very interesting. BecauseAjaxResponse
doesn't currently implement that. Is this just safeguarding a future where it will? Or is it safeguarding a future where we'll have a subclass ofAjaxResponse
that implements it?In fact, why not just make
AjaxResponse
implementCacheableResponseInterface
here, or at least add a@todo
to a follow-up issue?It does default to the empty array. So yes, that could be simplified to just:
Comment #264
effulgentsia CreditAttribution: effulgentsia at Acquia commentedFixes remaining failures and addresses #262 and #263.
Comment #265
Wim LeersIS updated.
#264 interdiff review:
I like how you linked to the "AJAX GET requests" issue :)
Is this actually correct?
Entire patch review:
Actually, I think
uncacheable
is wrong: the existingUrlGenerator
already allows you to collect cacheability metadata.What is different, is that HEAD's URL generator doesn't bubble cacheability metadata.
So what about s/uncacheble/non_bubbling/ ?
This this would become
CacheabilityBubblingUrlGenerator
, or perhaps even justBubblingUrlGenerator
(though that sounds like it's actually just an overengineered bubble bath).I wasn't able to find anything else! :) Updating the IS also helped me get a better/complete overview, hoping it'll help other reviewed (and the committer) too!
Comment #266
plachI think I can live with the requirement of manually dealing with URL cacheability for the advanced use cases, at least for now. In fact I still think we should aim for stopping to generate strings in controllers, just pass around domain objects representing our data model, and "render" them as a final step. For what this issue is concerned, this would mean passing around
Url
objects instead of converting them to strings. IMHO we should really try to get #2491981: There are too many ways to generate URLs and links done before the D8 release, as a pre-requisite.It would be nice to add some explicit test coverage for the cacheability of these (maybe as a follow-up?), right now we only know that they are not blowing up, I think :)
80 chars
Comment #267
effulgentsia CreditAttribution: effulgentsia at Acquia commentedWorking on #265 and #266.
Comment #268
effulgentsia CreditAttribution: effulgentsia at Acquia commentedIncorporated all the suggestions from those comments.
You're right. RedirectResponse adds a message body that's appropriate for 301 and 302, but not for 201, so fixed that.
I went with MetadataBubblingUrlGenerator.
Comment #269
plachThis looks ready to me, fixed a couple of nits.
Old array syntax.
Beautiful, this is how things should always work :)
Bogus trailing T
Comment #270
Wim Leers+1!
I also think this is ready now. Given that I've done an extensive review again at #265 and the interdiffs since then are small, plus the fact that @plach also thinks this is ready: RTBC!
Comment #271
Wim LeersComment #272
plachEven better :)
Comment #273
plachThe last change is minor to say the least, back to RTBC.
Comment #274
Fabianx CreditAttribution: Fabianx for Drupal Association commentedRTBC + 1 - This patch now really really truly looks awesome. The below is only things that I especially liked (and in one case a ask for documentation).
This is so much nicer, exactly how it should be.
That makes a lot of sense! :)
Oh, nice to use PSR-7 parts here. (unless that existed already before).
+1 much nicer.
We should probably document somewhere that no_cache has the side effect of removing the merge-in / bubble requirement. (no idea where though)
Proposed commit message:
Comment #275
catchOverall this looks great. It's a real shame we didn't get the URL generation APIs sorted out to support this properly, on the other hand it's great that render contexts added elsewhere makes this possible with minimal changes at this stage.
Found several minor things:
I think this needs to explain how it differs from/augments the cacheability metadata we already added to #type pager in #2433599: Ensure every (non-views) pager automatically associates a matching cache context.
This passes $return_as_object as a parameter, which means $access will always evaluate to TRUE. We should remove the if ($access).
Here it says pager_query_add_page() adds the cache context, but pretty sure above template_preprocess_pager() itself does? Also this is more or less the docs I was expecting to see above. I think this should if anything be an inline comment in the function, not in the docblock. The whole point of adding the cacheabiity metadata here is the person calling these functions doesn't need to do anything much. If they override, they'll probably look at the existing functions and see the inline docs.
Nit: shouldn't the summary be one line?
Not isRenderRoot? I can see arguments for either, just looks a bit odd to me.
This is new and only makes sense for<current>
right? Do we need to document that somewhere? And I'm wondering a bit if the generic property is really useful.(see below)
Nit: "in the way that"
OK so I see what we've done. Ignore the comment above - _no_path is just more generic than _no_fragment since we support query args too.
Does this mean different pagers to this one? It looks like a self-reference at the moment.
Comment #276
Wim Leers5. is pre-existing; this patch just moves it a bit. To answer the actual question: the current wording makes sense IMO: because it's also about render() calls *inside* renderRoot(), not only the call to renderRoot() itself.
Comment #277
plachI will take care of #274 and #275 tonight if none beats me to it.
Comment #278
alexpottI think we need a CR for this. Are there any other parts of this change that need a CR?
Comment #279
catchSorry I have another question that's less minor.
the
_no_cache
option on the route means it won't throw an exception if there's early-rendered content, that bit is OK for smart cache.However, setting _no_cache on a route does not disable render caching as such. This means that any code that's executed on the _no_cache route (say a block, but potentially within controller code) is 1. still render cached 2. will need the cacheable metadata of early-rendered URLs applied.
I think this works with the patch - you'd only need that metadata if you call render a cacheable render array within the controller, which in turn triggers additional early rendering/url() calls that don't bubble. However:
#1 it be good to double check that the above is really true, ideally with test coverage.
#2 I think people are going to see a route option like '_no_cache' and think it disables render caching altogether, which it currently doesn't do and likely shouldn't. I don't have good ideas for what to call this though instead.
@alexpott I thought about that when reviewing, but I'm not sure that specific change needs a change record. The only route that possibly has a use case for these is
<none>
- since how many routes without a path do you need?Comment #280
dawehnerWell, I think this will always just have one usecase the
<none>
route, anyway here is one: https://www.drupal.org/node/2532218Working on other feedback starting from the top now.
Comment #281
dawehnerImproved the docs
Its not always true ..., just in case
$element['#autocomplete_route_name']
was defined.Expanded the docs a bit.
Are you thinking of having two pagers both cached on the same page and ensure that they work independent from each other?
Comment #282
Fabianx CreditAttribution: Fabianx for Drupal Association commented#279: Yes, _no_cache currently only disables page cache and cache headers via a ResponsePolicy().
Comment #283
plachComment #284
catch@Fabianx well now it also disables early-rendered metadata bubbling, do we need a different route option for that then?
Comment #285
Fabianx CreditAttribution: Fabianx for Drupal Association commented#284:
Disclaimer: I was and am still a little against this change.
Reasoning:
This only bites a controller that returns a CacheableResponseInterface or a AttachmentsResponseInterface, but at the same time sets no_cache => TRUE.
- For attachments it makes no sense to disable early rendered metadata bubbling on no_cache - that points indeed to a new option.
- For cacheable it makes kinda sense, but why would you return a cacheable response if you don't want it cached?
The use case was that ResourceResponse implements CacheableResponseInterface and you could early render something within REST serializing and then still not want to cache it.
@plach / #283: If the patch would pass with that check removed, would you be okay to remove it for now and push the discussion to the follow-up already created?
If not, could we rename to:
_disable_metadata_bubbling or _override_metadata_bubbling_check?
Comment #286
webchickSince this issue is likely to be disruptive for contrib, tagging as a "beta target" for one to try and get in before the next beta (tentatively July 22).
Comment #287
plachOf course, we already did way more so far wrt my latest patches ;)
Comment #289
effulgentsia CreditAttribution: effulgentsia at Acquia commentedTo clarify, ideally, this issue will only be disruptive to contrib modules that implement controllers that return something other than a render array or AjaxResponse, and url() is called during that controller's execution, and there probably aren't too many of those. Once this patch lands, those modules will start triggering exceptions and need to be fixed. But additionally, production sites may experience some interesting side effects, such as new cache contexts added to some cached items (if that happens, probably a good thing, as the prior lack of them was probably a bug) and/or other performance/memory impacts, or even something due to a bug in this patch's logic that we all missed. So, for all those reasons, I agree with making this a beta target, even if the number of affected contrib modules ends up being low.
Comment #290
martin107 CreditAttribution: martin107 commentedI did not tackle the failing tests... yet
but while reading the patch over I picked out a couple of nits
The only one worth speaking about which is - locking the $option array to be an array.
Comment #291
martin107 CreditAttribution: martin107 commentedComment #293
Wim Leers#278
I somewhat disagree; it's a pure internal API change. OTOH, the most esoteric contrib code may also use this. But given that it's prefixed with an underscore, we already indicate it may change at any time, don't we?
It is a route option. Therefore, it applies to the route's controller, which generates an entire response, and not to the fragments that that response is made of.
So I think that's perfectly logical & valid.
no_fragment_cache: TRUE
makes more sense, to make it not render array-specific.#285:
Against
this, right? I agree it seems annoying/pointless for the common case, but it actually does make sense for routes like
system.batch_page.html
,system.batch_page.json
andsystem.db_update
. Those are the only 3 places in core that actually put this change inEarlyRenderingControllerWrapperSubscriber
to use. If we didn't have that, we'd have to untangle quite significant messes in there.Given that, I think the biggest criticism/concern in #285 is:
You usually would not. But perhaps you're modifying the result of another controller, or reusing big chunks of functionality from another module, and those happen to return
CacheableResponseInterface
response objects… in those situations it does become feasible to want to not cache those responses, but just because of code reuse you're technically working with cacheable responses.Finally, and this may be the strongest justification: you are building a site that has a few highly sensitive routes. So you set the
no_cache: TRUE
option, because due to their high sensitivity, you don't want to care about caching at all; you want to calculate everything, always. Note that this may also be the case for routes that were originally designed to be cached, but e.g. due to business requirements or super crazy advanced access requirements, you want to still mark an existing route asno_cache: TRUE
. I think it's fair for those routes to still setno_cache: TRUE
despite that not being entirely sensible; from those developers' POV, it totally is sensible!Comment #294
Fabianx CreditAttribution: Fabianx for Drupal Association commented#293: Lets move that discussion to a follow-up, plach wanted to check if its possible without that by now.
Comment #295
catchControllers don't only generate entire responses though, they generate render arrays which are converted to responses.
Comment #296
dawehnerWell, we need to tell people how to generate URLs on non render API requests. Seriously, it is not trivial, so providing some information in a CR is not a bad idea at all.
Comment #297
Wim Leers#2512132: Make CSRF links cacheable just landed; straight reroll. Next: updating accordingly, and working on fixing the failures in #290.
Comment #299
Wim LeersAs you can see, the exact same failures after #2512132: Make CSRF links cacheable; further showing that #2512132 was not disruptive.
Nevertheless, we should update relevant parts of this patch now that #2512132 has landed: we now deal with bubbleable metadata, not just cacheability metadata.
Comment #300
Wim LeersAnd this fixes those two failures that we saw in #281, #290, and my patches in #297 and (if all goes well) in #299.
Comment #301
Wim LeersSo, to get this issue back on track:
@alexpott/@dawehner: #278 + #293 + #296: CR for the
_only_fragment
->_no_path
change: exists at https://www.drupal.org/node/2532218. (I said I didn't think it was really necessary, but I'm of course okay with having it.) So this is addressed AFAICT.@Fabianx/@plach/@catch: #285: I answered to this in #293, then you briefly answered to that in #294. I'm not sure what #294 means though.
But #285 also asked for that "let
EarlyRenderingControllerWrapperSubscriber
NOT special caseno_cache
in this issue, leave that for a follow-up", which @plach +1'd in #287.So… can all three of you please confirm whether you'd like to see a patch with that removed? Would be happy to roll that. (Having deciphered the fragmented discussion in writing this, it seems that that is indeed what all of you would like to see.) The problem with that approach is (quoting myself from #293):
So, I'm fine with removing that
no_cache
stuff (it was added in #195 btw), but how do you then propose to handle those cases elegantly?(EDIT: the last link pointed to #191, it should've been #195. Fixed.)
Comment #303
plachI think the proposal was to remove the
no_cache
check only if that didn't introduce new test failures. If we want to do that anyway, I'd just wrap the various controllers in an::executeInRenderContext()
invocation and discard their metadata for now.Comment #304
Wim LeersThat's very fair. Doing that.
Comment #305
Wim LeersDone. I tried not to miss anything, let's see what testbot says.
(This removes the
no_cache
route option exclusion logic. And instead fixes the places that cause exceptions; they're all places where cacheability makes no sense at all — all batch stuff.)Comment #307
Wim LeersThe failures were in now-obsolete tests, that were specifically for the
no_cache
stuff.So, given that, I think the answer is clear: yes, we can do this patch without the
no_cache
exclusion. Hurray!Comment #308
Wim LeersI did one more round of reviews, and found a few spots I missed in #299.
I've been posting patches in the last two days, but: #297 + #299 + #300 were just for chasing HEAD after #2512132: Make CSRF links cacheable landed (and were 99% string replacement), and #305 + #307 were almost trivial rerolls to address the feedback since this was un-RTBC'd in #275.
Given that, I think I still qualify to review this issue. I now agree with Fabianx'/catch's concerns about
no_cache
. I think this patch is ready now. I'll leave it to others to RTBC though.Comment #309
plachInterdiffs look good to me: since this was already RTBC and I didn't work on it after that, it should be fine for me to RTBC it again.
Comment #310
Fabianx CreditAttribution: Fabianx for Drupal Association commentedRTBC + 1 - It is great that that other issue landed first! :)
Comment #311
catchAll the interdiffs look a lot better.
With render contexts I'm wondering a bit whether we actually should be supporting $variables['#attached'] etc. or whether we should encourage people to do $variables['actual_variable']['#attached'] instead (so that attachments only get applied when the variable is printed), but that's more an discussion for #2528314: template_preprocess_node() does not add cacheability metadata than here.
Committed/pushed to 8.0.x, thanks!
Comment #313
Wim LeersThis issue also fixed #2305013: Add a helper method to the Url class to render just the query string and fragment, so marked that as a duplicate.
Comment #314
mikeker CreditAttribution: mikeker as a volunteer commentedFollowup from this issue: #2539472: Pager "first" and "previous" links have incorrect URLs.