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.

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.

Support from Acquia helps fund testing for Drupal Acquia logo

Comments

rfay’s picture

subscribe

sun’s picture

Status: Active » Needs review
FileSize
771 bytes

Hm. Let's start with this?

effulgentsia’s picture

Status: Needs review » Active

Ok 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?

effulgentsia’s picture

Assigned: effulgentsia » Unassigned

Unassigning myself. I may post ideas at some point, but I encourage others to as well.

effulgentsia’s picture

Version: 7.x-dev » 8.x-dev
JeremyFrench’s picture

A '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.

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']

Why is this less secure for Ajax forms than standard forms?

tim.plunkett’s picture

Issue tags: +VDC

Views has a hack for this, it'd be nice to remove it: http://api.drupal.org/api/search/7/views_ui_ajax_get_form

sun’s picture

Issue tags: +Form system

I 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.

tim.plunkett’s picture

Sure, a one-way boolean sounds fine to me.

effulgentsia’s picture

Cross-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:

Why is this less secure for Ajax forms than standard forms?

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).

andrewbelcher’s picture

Just 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?

iamEAP’s picture

Issue tags: +Needs backport to D7

Would an acceptable solution be to make it so that anonymous AJAX form caches never expire?

This 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.

#1286154: Allow custom default form cache expiration/lifetime may provide a solution to that by allowing forms to define their cache length?

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.

iamEAP’s picture

Status: Active » Needs review
FileSize
706 bytes

Something like this.

iamEAP’s picture

Note there seem to be a number of related but nevertheless distinct issues at play here. My summary below:

  1. Bug: Form API, the AJAX part of it in particular, relies on form cache persistence and availability at an architectural level
  2. Bug: Form cache entries can expire before their host page cache entries (because page cache min/max is configurable up to 24 hours while form cache is hard-coded at 6)
  3. Feature request: Allow custom form cache expiration/lifetime

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).

iamEAP’s picture

Issue tags: -Needs backport to D7, -VDC, -Form system

Status: Needs review » Needs work
Issue tags: +Needs backport to D7, +VDC, +Form system

The last submitted patch, drupal-ajax_form_cache-774876-14.patch, failed testing.

iamEAP’s picture

Status: Needs work » Needs review
FileSize
3.73 KB

Guessing there are some unit tests that this will break, but here's #14, now that everything's been moved to FormBuilder.

iamEAP’s picture

And, yup. Here are the PHPUnit test fixes. Not adding any tests, though.

Status: Needs review » Needs work

The last submitted patch, drupal-ajax_form_cache-774876-19.patch, failed testing.

iamEAP’s picture

And... Yup. install_begin_request

Just adding the argument to form_builder constructor there.

iamEAP’s picture

Status: Needs work » Needs review
tim.plunkett’s picture

Issue tags: +Needs tests, +PHPUnit

Let's ensure that we get unit test coverage, now that we can.

iamEAP’s picture

Adding 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.

iamEAP’s picture

Whoops. The interdiff above (#24) is valid. Just forgot to add the changes in the interdiff to the actual patch itself. That's here:

iamEAP’s picture

Per 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.

tim.plunkett’s picture

Issue tags: -Needs tests

Yes, that is much more readable.

  1. +++ b/core/lib/Drupal/Core/Form/FormBuilder.php
    @@ -133,6 +133,11 @@ class FormBuilder implements FormBuilderInterface {
    +   * A reasonable minimum cache TTL for form cache entries.
    ...
    +  const MIN_CACHE_TTL = 21600;
    
    @@ -402,9 +407,9 @@ public function getCache($form_build_id, &$form_state) {
    +    $expire = max(self::MIN_CACHE_TTL, $page_max_age);
    

    Isn't this the MAX_CACHE_TTL? Also, please use static:: not self::

  2. +++ b/core/tests/Drupal/Tests/Core/Form/FormBuilderTest.php
    @@ -487,67 +487,130 @@ public function testGetCache() {
    +    $builder_class = get_class($this->formBuilder);
    ...
    +        'cache.page.max_age' => $builder_class::MIN_CACHE_TTL - 120,
    

    I think its safe to just use TestFormBuilder:: here, but it doesn't bother me much either way.

iamEAP’s picture

The 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.

tim.plunkett’s picture

That explanation makes sense, thanks.

One last nitpick!

+++ b/core/lib/Drupal/Core/Form/FormBuilder.php
@@ -135,6 +148,8 @@ class FormBuilder implements FormBuilderInterface {
+   * @param \Drupal\Core\Config\ConfigFactory $config

This should be $config_factory.

Also, can you rename the issue? The title doesn't seem to agree with what we're changing here.

iamEAP’s picture

Title: AJAX form submissions don't work unless the form can be retrieved from cache » Form cache entries can expire before their host page's cache entry
FileSize
698 bytes
12.06 KB

Updating the title and attaching a patch to address #29.

I'll also be updating the issue summary shortly.

iamEAP’s picture

Issue summary: View changes

Updating issue to reflect latest solution, crosslink related issues, and use the issue summary template.

Status: Needs review » Needs work
Issue tags: -PHPUnit, -Needs backport to D7, -VDC, -Form system

The last submitted patch, drupal-ajax_form_cache-774876-30.patch, failed testing.

iamEAP’s picture

Status: Needs work » Needs review
iamEAP’s picture

Status: Needs review » Needs work
Issue tags: +PHPUnit, +Needs backport to D7, +VDC, +Form system

The last submitted patch, drupal-ajax_form_cache-774876-30.patch, failed testing.

tim.plunkett’s picture

Status: Needs work » Needs review
FileSize
12.17 KB

Rerolled.

tim.plunkett’s picture

Issue summary: View changes

Clarifying / fixing some grammar issues in the summary.

fago’s picture

+++ b/core/lib/Drupal/Core/Form/FormBuilder.php
@@ -391,8 +407,10 @@ public function getCache($form_build_id, &$form_state) {
+    // Form cache expiration should be at least the max age of the page itself
+    // to ensure AJAX functionality.
+    $page_max_age = $this->configFactory->get('system.performance')->get('cache.page.max_age');
+    $expire = max(static::MIN_CACHE_TTL, $page_max_age);

hm, 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?

iamEAP’s picture

Issue summary: View changes
FileSize
12.44 KB

Reroll attached.

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?

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.

Status: Needs review » Needs work

The last submitted patch, 37: drupal-ajax_form_cache-774876-37.patch, failed testing.

Version: 8.0.x-dev » 8.1.x-dev

Drupal 8.0.6 was released on April 6 and is the final bugfix release for the Drupal 8.0.x series. Drupal 8.0.x will not receive any further development aside from security fixes. Drupal 8.1.0-rc1 is now available and sites should prepare to update to 8.1.0.

Bug reports should be targeted against the 8.1.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.2.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

cilefen’s picture

Component: ajax system » forms system
Priority: Normal » Major

Is 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.

Version: 8.1.x-dev » 8.2.x-dev

Drupal 8.1.9 was released on September 7 and is the final bugfix release for the Drupal 8.1.x series. Drupal 8.1.x will not receive any further development aside from security fixes. Drupal 8.2.0-rc1 is now available and sites should prepare to upgrade to 8.2.0.

Bug reports should be targeted against the 8.2.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.3.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Reuben Unruh’s picture

Status: Needs work » Closed (duplicate)
tim.plunkett’s picture

Status: Closed (duplicate) » Needs work

@Reuben Unruh, which issue is this a duplicate of?

Reuben Unruh’s picture

Sorry! 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.

xjm’s picture

Title: Form cache entries can expire before their host page's cache entry » Form cache entries can expire before their host page's cache entry, resulting in all AJAX failing
Status: Needs work » Postponed (maintainer needs more info)
Issue tags: +Needs triage for core major current state

So 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.

Version: 8.2.x-dev » 8.3.x-dev

Drupal 8.2.6 was released on February 1, 2017 and is the final full bugfix release for the Drupal 8.2.x series. Drupal 8.2.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.3.0 on April 5, 2017. (Drupal 8.3.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.3.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.4.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.3.x-dev » 8.4.x-dev

Drupal 8.3.6 was released on August 2, 2017 and is the final full bugfix release for the Drupal 8.3.x series. Drupal 8.3.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.4.0 on October 4, 2017. (Drupal 8.4.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.4.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.5.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

wizonesolutions’s picture

Issue tags: +Vienna2017

Triaging this issue at DrupalCon Vienna with @Stela Oroz

wizonesolutions’s picture

@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.

Version: 8.4.x-dev » 8.5.x-dev

Drupal 8.4.4 was released on January 3, 2018 and is the final full bugfix release for the Drupal 8.4.x series. Drupal 8.4.x will not receive any further development aside from critical and security fixes. Sites should prepare to update to 8.5.0 on March 7, 2018. (Drupal 8.5.0-alpha1 is available for testing.)

Bug reports should be targeted against the 8.5.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.6.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.5.x-dev » 8.6.x-dev

Drupal 8.5.6 was released on August 1, 2018 and is the final bugfix release for the Drupal 8.5.x series. Drupal 8.5.x will not receive any further development aside from security fixes. Sites should prepare to update to 8.6.0 on September 5, 2018. (Drupal 8.6.0-rc1 is available for testing.)

Bug reports should be targeted against the 8.6.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.7.x-dev branch. For more information see the Drupal 8 minor version schedule and the Allowed changes during the Drupal 8 release cycle.

Version: 8.6.x-dev » 8.8.x-dev

Drupal 8.6.x will not receive any further development aside from security fixes. Bug reports should be targeted against the 8.8.x-dev branch from now on, and new development or disruptive changes should be targeted against the 8.9.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.8.x-dev » 8.9.x-dev

Drupal 8.8.7 was released on June 3, 2020 and is the final full bugfix release for the Drupal 8.8.x series. Drupal 8.8.x will not receive any further development aside from security fixes. Sites should prepare to update to Drupal 8.9.0 or Drupal 9.0.0 for ongoing support.

Bug reports should be targeted against the 8.9.x-dev branch from now on, and new development or disruptive changes should be targeted against the 9.1.x-dev branch. For more information see the Drupal 8 and 9 minor version schedule and the Allowed changes during the Drupal 8 and 9 release cycles.

Version: 8.9.x-dev » 9.2.x-dev

Drupal 8 is end-of-life as of November 17, 2021. There will not be further changes made to Drupal 8. Bugfixes are now made to the 9.3.x and higher branches only. For more information see the Drupal core minor version schedule and the Allowed changes during the Drupal core release cycle.

Version: 9.2.x-dev » 9.3.x-dev
quietone’s picture

Status: Postponed (maintainer needs more info) » Closed (outdated)
Issue tags: -Form system +Bug Smash Initiative

More 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!