Updated: Comment #30
Problem/Motivation
Currently, page cache entries are configurable through the Drupal UI up to 24 hours. Form cache entries are hard-coded with TTLs of 6 hours. In situations where site administrators configure page cache max age greater than 6 hours, page cache entries containing forms (and form build IDs) can correspond to non-existent form cache entries. In essence, were one to configure page cache max age to 24 hours, there would be an 18 hour period during which any form on a given page would not have a corresponding form cache entry.
In such situations, one of the worst symptoms is a complete failure of all AJAX-enabled functionality on the form (because AJAX-enabled fields rely on a persistent, associated cache entry).
Proposed resolution
We should supply a reasonable minimum form cache TTL (we can keep it 6 hours), but scale up dynamically in situations where page cache max age has been configured higher.
Remaining tasks
n/a
User interface changes
n/a
API changes
We're adding an argument the constructor for FormBuilder, but I'm not sure we consider those API changes.
Related Issues
Original report by effulgentsia
Currently, ajax_get_form() can only retrieve the form from cache, not build from $form_id as drupal_build_form() can. The reason is that there's no way for it to know what the $form_id is securely: you can't trust $_POST['form_id']. One problem this creates is that an AJAX-enabled form may be retrieved from the page cache after the corresponding form build expired from the form cache. Another problem is just the WTF that this is the only place within core where $form_state['cache'] is required on the first step of a form. I have some ideas that I'll post when I can work them out.
Comment | File | Size | Author |
---|---|---|---|
#37 | drupal-ajax_form_cache-774876-37.patch | 12.44 KB | iamEAP |
#35 | ajax_form_cache-774876-35.patch | 12.17 KB | tim.plunkett |
#30 | drupal-ajax_form_cache-774876-30.patch | 12.06 KB | iamEAP |
#30 | drupal-ajax_form_cache-774876-30.interdiff.txt | 698 bytes | iamEAP |
#28 | drupal-ajax_form_cache-774876-28.patch | 12.05 KB | iamEAP |
Comments
Comment #1
rfaysubscribe
Comment #2
sunHm. Let's start with this?
Comment #3
effulgentsia CreditAttribution: effulgentsia commentedOk as a start, but what I opened this issue for was to explore whether it's possible for AJAX to have a trustworthy $form_id, and be able to reconstruct the form if it's not in cache, as drupal_build_form() is able to do. Not sure if it's possible for D7 yet (especially, if all of us are prioritizing the criticals, as we should be), but doesn't hurt to leave this as an "active" non-critical issue, does it?
Comment #5
effulgentsia CreditAttribution: effulgentsia commentedUnassigning myself. I may post ideas at some point, but I encourage others to as well.
Comment #6
effulgentsia CreditAttribution: effulgentsia commentedComment #7
JeremyFrench CreditAttribution: JeremyFrench commentedA 'solution' for this should be to set your page cache to three hours, as forms are by default cached for 6 hours. Not great but it should stop AJAX forms from failing.
Why is this less secure for Ajax forms than standard forms?
Comment #8
tim.plunkettViews has a hack for this, it'd be nice to remove it: http://api.drupal.org/api/search/7/views_ui_ajax_get_form
Comment #9
sunI also ran into this in the Mollom module (which requires form caching to persist data relevant to a particular form submission).
The architectural flaw is that any module can go into the form at any time and just simply override #cache to FALSE.
(there are some funky contrib modules that do this for very odd reasons)
To prevent this from happening, in an ideal world, $form would have to be a class with a
$form->enableCache()
method, without a counterpart; i.e., without any chance for anyone else to revert that decision once enabled.Of course, that doesn't help with the other AJAX/form-cache expiration issue mentioned in the OP, but at the same time, that doesn't look resolvable to me in any way in the first place.
Comment #10
tim.plunkettSure, a one-way boolean sounds fine to me.
Comment #11
effulgentsia CreditAttribution: effulgentsia commentedCross-linking some related issues: #1761488: Process forms in a request listener and #597280: Introduce form registry to fix various problems and increase security.
Both this issue and #1761488: Process forms in a request listener require that we be able to call drupal_get_form($_POST['form_id']) and not have it execute an arbitrary function. A registration mechanism ala #597280: Introduce form registry to fix various problems and increase security that also includes access control info is one possible way to achieve that.
Re #7:
For standard forms, we never call drupal_get_form($_POST['form_id']). We invoke a 'page callback' from a menu router item or a block. Those are access controlled and they call drupal_get_form($some_string_that_is_not_user_input).
Comment #12
andrewbelcher CreditAttribution: andrewbelcher commentedJust wondering if there was any progress on this? #1694574: drupal_process_form() deletes cached form + form_state despite still needed for later POSTs with enabled page caching is about invalid caches on login which I think can be solved by one of a couple fairly easy options, but solving that just postpones the problem until we hit the 6 hours...
Would an acceptable solution be to make it so that anonymous AJAX form caches never expire? That way they will be valid for the duration of the page cache. This could obviously be tied into whether anonymous caching is enabled to make it only happen when relevant? #1286154: Allow custom default form cache expiration/lifetime may provide a solution to that by allowing forms to define their cache length?
Comment #13
iamEAP CreditAttribution: iamEAP commentedThis would probably result in a whole lot of garbage in your form cache. I also think it'd be difficult to reliably determine which forms have AJAX-enabled fields and which forms do not.
Probably not a good idea; unless you're deeply in-tune with the underpinnings of FAPI (and I don't think anyone really is, including the maintainers), you're probably going to shoot yourself in the foot.
I think a reasonable solution is to make the default value somewhat dynamic: e.g. form expiration time = REQUEST_TIME + $EXPIRE where EXPIRE is MAX(6 hours, page cache minimum lifetime).
Also, adding a needs backport tag because this needs backport.
Comment #14
iamEAP CreditAttribution: iamEAP commentedSomething like this.
Comment #15
iamEAP CreditAttribution: iamEAP commentedNote there seem to be a number of related but nevertheless distinct issues at play here. My summary below:
The patch above solves #2.
#1 is still a valid issue, but decoupling FAPI and form cache sounds unresolvable (as I believe @sun alluded to in #9) and at least this patch mitigates the possibility of issues cropping up.
#3 is also still a valid feature request, though its need is significantly reduced. Specifically, the only thing it would enable is a site admin to set form cache expiration greater than page cache expiration (which would only be handy in cases where people AJAX submit forms after having stayed on the page more than 6, 12, 24, etc hours).
Comment #16
iamEAP CreditAttribution: iamEAP commented#14: drupal-ajax_form_cache-774876-14.patch queued for re-testing.
Comment #18
iamEAP CreditAttribution: iamEAP commentedGuessing there are some unit tests that this will break, but here's #14, now that everything's been moved to FormBuilder.
Comment #19
iamEAP CreditAttribution: iamEAP commentedAnd, yup. Here are the PHPUnit test fixes. Not adding any tests, though.
Comment #21
iamEAP CreditAttribution: iamEAP commentedAnd... Yup. install_begin_request
Just adding the argument to form_builder constructor there.
Comment #22
iamEAP CreditAttribution: iamEAP commentedComment #23
tim.plunkettLet's ensure that we get unit test coverage, now that we can.
Comment #24
iamEAP CreditAttribution: iamEAP commentedAdding tests for FormBuilder::setCache(). Trying to test all scenarios covered in code, currently: authenticated, unauthenticated, page cache max age > 6hrs, page cache max age < 6hrs.
To do the latter pair, I had to add a config factory setter on the FormBuilder test wrapper.
Comment #25
iamEAP CreditAttribution: iamEAP commentedWhoops. The interdiff above (#24) is valid. Just forgot to add the changes in the interdiff to the actual patch itself. That's here:
Comment #26
iamEAP CreditAttribution: iamEAP commentedPer discussion with @tim.plunkett on IRC, I'm breaking out all of those scenarios described in #24 into separate test methods. Much more readable and maintainable.
Also, moved the 6 hour minimum to a constant (FormBuilder::MIN_CACHE_TTL) on the FormBuilder class to ensure the test is properly decoupled.
Comment #27
tim.plunkettYes, that is much more readable.
Isn't this the MAX_CACHE_TTL? Also, please use static:: not self::
I think its safe to just use TestFormBuilder:: here, but it doesn't bother me much either way.
Comment #28
iamEAP CreditAttribution: iamEAP commentedThe min vs. max gets pretty semantically weedy. I think it could reasonably be considered max if keyValueStoreInterface::get() explicitly required that implementors not return items past expiration (which is not the case, I believe). Historically, Drupal's used "minimum cache lifetime" because we leave garbage collection to cron; min TTL just means: "I guarantee this will be in cache until a certain point, regardless of garbage collection. After that time, you may still get cache hits (because garbage collection hasn't run yet), but I cannot guarantee that."
Beyond that, though, I simply mean "minimum" because it could be more than that value in situations where page cache max age is greater. I'd also be fine making it ambiguous (e.g. FormBuilder::CACHE_TTL).
As for using TestFormBuilder instead of loading the class, I'll leave as is (just to keep the only hard-coded instance of that in the setUp method).
Duly noted on static::MIN_CACHE_TTL, though. Fix for that attached here.
Comment #29
tim.plunkettThat explanation makes sense, thanks.
One last nitpick!
This should be $config_factory.
Also, can you rename the issue? The title doesn't seem to agree with what we're changing here.
Comment #30
iamEAP CreditAttribution: iamEAP commentedUpdating the title and attaching a patch to address #29.
I'll also be updating the issue summary shortly.
Comment #30.0
iamEAP CreditAttribution: iamEAP commentedUpdating issue to reflect latest solution, crosslink related issues, and use the issue summary template.
Comment #32
iamEAP CreditAttribution: iamEAP commented#30: drupal-ajax_form_cache-774876-30.patch queued for re-testing.
Comment #33
iamEAP CreditAttribution: iamEAP commented#30: drupal-ajax_form_cache-774876-30.patch queued for re-testing.
Comment #35
tim.plunkettRerolled.
Comment #35.0
tim.plunkettClarifying / fixing some grammar issues in the summary.
Comment #36
fagohm, if you run a site with a high page cache ttl, but lots of forms for authenticated users - one will end up with lots of form cache entries :/
However, this is generally problematic and might cause problems also with block caching or panels caching. But the form, cannot know how it is cached.
However, we could allow setting a minimum cache expiration time per form, so if you have forms being cached longer you can increase it specifically for those forms?
Comment #37
iamEAP CreditAttribution: iamEAP commentedReroll attached.
This patch at least enables this type of functionality by making the cache TTL a constant on the FormBuilder class. If necessary, the class can be extended to provide custom functionality (like a per-form ttl; presumably, this could be provided by contrib). What's proposed here seems like a reasonable "80%" solution.
Comment #41
cilefen CreditAttribution: cilefen commentedIs this still an issue in 8.1.x? If so, this is major according to the priority guidelines. It looks like the caching logic is now in FormCache.
Comment #43
Reuben Unruh CreditAttribution: Reuben Unruh commentedComment #44
tim.plunkett@Reuben Unruh, which issue is this a duplicate of?
Comment #45
Reuben Unruh CreditAttribution: Reuben Unruh commentedSorry! I added the related issue but I see I'm supposed to add a link as a comment also to call out a duplicate.
#1286154: Allow custom form cache expiration/lifetime is about making the form cache expiry configurable. The most recent patch there doesn't seem to ensure that the expiry matches the page cache expiry but it is mentioned in the issue summary that this breaks ajax. Also, there doesn't seem to be any activity here.
I didn't update #1286154 at all.
Comment #46
xjmSo I don't think we should close this as a duplicate of a much more general feature request if the bug described in the summary still exists, because it is actually quite nasty.
However, we should confirm whether this is still a problem in 8.2.x, since there was a lot of change to this functionality since 2013.
Comment #49
wizonesolutionsTriaging this issue at DrupalCon Vienna with @Stela Oroz
Comment #50
wizonesolutions@tim.plunkett @fago @iamEAP Can any of you elaborate on the steps to reproduce for this issue? We would like to see if it still happens.
Comment #57
quietone CreditAttribution: quietone at PreviousNext commentedMore information about this issue was asked for in #50, 5 years. No additional information has been supplied, therefor closing.
If you are experiencing this problem on a supported version of Drupal reopen the issue, by setting the status to 'Active', and provide complete steps to reproduce the issue (starting from "Install Drupal core").
Thanks!