Problem/Motivation
The Datetime and Datelist element has intricate and opaque logic to handle timezones.
It only works if the timezone intended by the user matches one of the following 4 (pseudocode) circumstances:
1) #default_value->getTimezone()->getName() === drupal_get_user_timezone() === INTENDED; or
2) #default_value->getTimezone()->getName() === #date_timezone === INTENDED; or
3) empty(#default_value) && !empty(#date_timezone) && (#date_timezone === INTENDED); or
4) empty(#default_value) && (drupal_get_user_timezone() === INTENDED).
If you set up your element in any different way, your data get mangled.
The docs hint at this fragility:
- #date_timezone: The local timezone to use when creating dates. Generally
* this should be left empty and it will be set correctly for the user using
* the form. Useful if the default value is empty to designate a desired
* timezone for dates created in form processing. If a default date is
* provided, this value will be ignored, the timezone in the default date
* takes precedence. Defaults to the value returned by
* drupal_get_user_timezone().
But they are wrong. The problems are:
1) If a default date is provided, its timezone will be used to present the date to the user, but a different timezone (either #date_timezone if present or drupal_get_user_timezone otherwise) will be used to interpret the date when the element is saved. So just loading the form and pressing save will cause a change in the saved time.
2) If the element is set up with a #date_timezone value, and there is no #default_value, then the value of #date_timezone is reset in ::processDatetime to drupal_get_user_timezone(). In other words, later stages of the element render process get misled about what timezone will be assumed.
3) ::processDatetime contains a reference to an undocumented #timezone property that is unknown in core.
Proposed solution
1) Make #date_timezone the determinant of what timezone is used to interpret values in ::valueCallback.
2) Make sure #date_timezone accurately informs later stages of the render process what timezone will be used to interpret values when the form is saved.
3) Remove the reference to the phantom #timezone property.
This will result in the docs saying:
- #date_timezone: The local timezone to use when displaying
* or interpreting dates. Defaults to the value returned by
* drupal_get_user_timezone().
| Comment | File | Size | Author |
|---|---|---|---|
| #77 | 2799987-77.patch | 21.1 KB | jofitz |
| #74 | 2799987-74.patch | 21.05 KB | jofitz |
| #69 | interdiff-60-69.txt | 10.12 KB | Anonymous (not verified) |
| #69 | 2799987-69.patch | 21.07 KB | Anonymous (not verified) |
| #60 | 2799987-60.patch | 21.12 KB | oriol_e9g |
Comments
Comment #2
goz commentedComment #3
goz commentedComment #4
mpdonadioThis will be 8.2.x eligible, but since this is green it means we are missing test coverage for this behavior.
Also think we need to add this behavior to Element\DateElementBase so it can be used in both Element\Datelist and Element\Datetime; right now Datelist and Datetime have different logic here (a quick git blame looks like this has been here since the beginning).
Comment #5
mpdonadioComment #6
goz commentedI make more investigation, and looks like #timezone is never setted or used. It's only tested in Datetime::processDatetime and Datelist::processDatelist. I can't find other occurences instead of webform contrib module, even in previous date.module from D7.
Removing code, especially unexplained code is risky. Maybe someone who know more about this element could help us figure out what #timezone is supposed to do.
The first time #timezone appears is in https://www.drupal.org/node/501428#comment-6673762 by KarenS during port from date and time to core. But i still don't understand the purpose of this attribute and which code use it elsewhere.
Comment #7
Anonymous (not verified) commented+1 for GoZ. It really looks like strange logic (and two different logics):
Datelist:
Datetime:
Also i'm not found
#timezonein all 2361 Drupal 8 modules on current moment (webform-8.x-5.x-dev haven't#timezonetoo).Comment #8
mpdonadioCan we move this logic into DateElementBase::processDatetime() and call it from both elements?
We also need a test to demonstrate the problem and to prevent a regression down the road. Probably a kernel test in Drupal\KernelTests\Core\Element?
Comment #9
Anonymous (not verified) commentedI tried to do #8, but I have some problems with it. All main points in
valueCallback(),processDatelist()andvalidateDatelist()are triggered on$date instanceof DrupalDateTime. If i'm pass other type, it responsenullvalue, or array with'object' => null. Hence i cann't demonstrate the problem with bad '#timezone'. But i'm still think that we should remove this logic:because it just haven't sense :)
Also i wanted move the logic into DateElementBase::processDatetime(), but I don't know what to do with this line:
$date = !empty($element['#value']['object']) ? $element['#value']['object'] : NULL;because we need $date for both places, but we don't need c/p code. Or create special method for this with
return $date?Comment #10
mpdonadioComment #11
mpdonadioThis is what I was thinking of for the common function. No need to worry about duplicated line; and we want it pas part of the process list anyway.
Need to look at the test later.
Comment #13
jonathanshawI think I understand why this line is there. It's connected with something I noted in #2866402: Datetime element's timezone handling is fragile :
If #date_timezone is present, and $date is not a DrupalDateTime and #timezone is empty, then the value of #date_timezone is reset to drupal_get_user_timezone(). But when the form gets saved and the valueCallback gets called again, it will be interpreting times using the original value of #date_timezone regardless of the value set here.
So setting a #timezone property on the element (with value identical to #date_timezone) is a way to make #date_timezone stay correct throughout.
This is of course a stupid, fragile and undocumented hack that it's unlikely anyone is actually using. But perhaps thinking along these lines is the historical cause of what we've got here.
Regardless, I think we should close this issue and find a more comprehensive solution in #2866402: Datetime element's timezone handling is fragile .
Comment #14
jonathanshaw@mpdonadio wants to continue in this issue and close #2866402: Datetime element's timezone handling is fragile .
Here's a kernel test that explores the oddities I pointed out there. I'll update the IS here tomorrow, integrate patches, etc.
Comment #16
jonathanshawOK, here are 2 possible solutions:
"clean" which makes things very simple, and "maxBC" which preserves as much BC as possibility for people who are extending this element's class and modifying ::valueCallback or ::getInfo but not ::processDatetime.
I haven't followed @mpdonadio's wish to add a separate process handler for timezones, as this is more complicated than my "clean" solution but less BC preserving than my "maxBC". Given the additional problems that we're now trying to address here, I don't see how it helps us.
(The interdiff shows the differences between these solutions, not with previous patches as these are different approaches to previous).
Comment #17
jonathanshawComment #18
jonathanshawMajor as it can corrupt data and is needed as a prerequisite for more complex time zone handling in #2632040: [PP-1] Add ability to select a timezone for datetime field, which is a major feature request?
Comment #22
mpdonadioThe reason for this is because the same general problem exists in the Datelist element, so ideally we would centralize logic in the base class.
Comment #23
jonathanshawI figured so - but the "clean" solution I suggest removes the need for this kind of code. The basic idea of it is that things already go wrong if #date_timezone is not set AND we don't want to use the user timezone. So therefore we can default #date_timezone to the user timezone, and trust that it is always there and always correct - there's no need to update it. This is the only setup that actually works currently, so we don't need to support more complex setups.
We should probably put an additional process handler in for #2632040: [PP-1] Add ability to select a timezone for datetime field though, as we will then need to update #date_timezone, to reflect the element user's selection of timezone
Comment #25
jhedstromI really like the 'clean' patch from above much more than the max bc. If anybody is already extending this and messing about with the
date_timezoneelement, it's probably being set/overridden by that extending class, so it's possible perhaps to go with the clean solution here?Comment #26
mpdonadioI think we need to expand the #16-clean patch to include Datelist.
Comment #27
jonathanshawAs requested, #16-clean patch expanded to cover the Datelist elemet as well as Datetime element. This involved quite a bit lot of refactoring of the tests, making the interdiff for the tests (which are 95% of code here) hard to follow.
We have an agreed solution and tests, what we need now is code review of the tests and then we are RTBC.
Comment #28
jonathanshawComment #29
jonathanshawSorry, forgot to update the API doc strings as described in the IS. Only a change to the comments, so test-only patch from #27 is still valid.
Comment #31
jonathanshawI've drafted a change record: https://www.drupal.org/node/2880055. Please review.
Self-review of patch line 350:
$message = "The correct timezone should be set on the processed datetime elements: (expected, actual) \n" . print_r($wrongTimezones, TRUE);needs to be $this->elementType not 'datetime' in the $message string.
Comment #32
mpdonadioI think this looks good. These are some nits.
I want to run the tests locally and play with them before RTBC. This is the weird situation of both a doc bug and a code bug, so I think we will want a Change Record.
Leaving at NR and assigning to myself so I remember to do a final look.
We typically don't add use statements for PHP classes in the root namespace.
Think this is going to warrant a Change Record.
Should be alphabetical.
Needs blank line before.
Can use assertEquals (fails show better info to help with debug).
Can use assertCount().
Comment #33
jonathanshawAddresses #31 and #32. Thanks for the quick review @mpdonadio!
The change record is at https://www.drupal.org/node/2880055.
Comment #34
jonathanshawComment #35
mpdonadioAre we confident that the test-only patch demonstrates the problem, and just covers the tweaks we made to the logic:
I think this does, but want a second opinion as the TZ problems can be maddening at times.
Two small nits, but I am still reviewing the tests.
This my bad; I shouldn't have asked for this. The blank between the comment and the class opening needs to be removed (the sniff is "There must be exactly one newline after the class comment"
This needs a @return. Oddly, PHPCS missed this, but PhpStorm complained when
Comment #36
jhedstromComment #37
jonathanshaw@mpdonadio wanted it left at NR in #32 so he can do a final review, which he said in #34 he was only part way through.
We also need a second review opinion on the point in #34; it's my IS being discussed so I can't give it.
Comment #38
jhedstromAh, back to NR then :) Had set to NW for #35.
Comment #42
jhedstromRe #35
I agree that the test demonstrates the issue here after another walk through it.
Back to NW for the nits in #35.
Comment #43
jonathanshawQueueing multiple tests because suspecting a random fail on https://www.drupal.org/pift-ci-job/868537.
Comment #44
jonathanshawWe have a clear IS, we have thorough tests and test-only patch (#33), we have a change record, we have agreement in principle from @mpdonadio, we have reviews from @mpdonadio and @jhedstrom.
Are you OK to RTBC @jhedstrom?
Comment #45
jonathanshawComment #46
jonathanshawComment #47
jonathanshawComment #49
jonathanshawProbably a windows/linux issue causing #44 to not apply, let's hope someone with a better dev environment can fix this.
Comment #50
mpdonadioThe patch in #44 is UTF-16 encoded; that's why it won't apply.
Comment #51
harsha012 commentedre-rolled the patch
Comment #52
jonathanshawThe PHP 7 tests fails are due to #2918570: Drupal\KernelTests\Core\Image\ToolkitGdTest fails on PHP 7.1.x-dev and 7.0.x-dev following a PHP bugfix, not a problem in this patch.
Comment #53
MixologicPHP7 test fails were due to the fact that the patch was tested against 8.3.x, instead of 8.6.x. 8.3.x does not have the fix from #2918570: Drupal\KernelTests\Core\Image\ToolkitGdTest fails on PHP 7.1.x-dev and 7.0.x-dev following a PHP bugfix
Comment #54
MixologicComment #55
jofitzComment #56
oriol_e9gComment #57
oriol_e9gThis is only a reroll from #51 I have seen that all comments in #35 was addressed in the last patch.
I have only fixed one nit from #51:
+ '#date_timezone' => drupal_get_user_timezone() ,->
+ '#date_timezone' => drupal_get_user_timezone(),Comment #58
Anonymous (not verified) commentedChange records was done in #33. My patches from #7 and #9 are not relevant to the current version. Looks like all requests from @mpdonadio and @jhedstrom were adressed. Great works!
Only one nit CS typo remained, it would be cool to solve it before raising the status to RTBС.
Comment #59
oriol_e9gSorry no interdiff 57 > 59
Comment #60
oriol_e9gAnd now the last CS nit.
Comment #61
Anonymous (not verified) commentedLooks perfect!
Comment #63
Anonymous (not verified) commentedComment #65
jonathanshawComment #66
larowlanThanks for the effort here, this is a really thorough test, great work.
Just a couple of minor nits/typos.
In terms of the #timezone origins. It looks like it came in with the original patch to add date field to core. I looked into the Date module's history and could not find any places where it existed in their git history, so I suspect it came in with the original date patch.
Searching through core's git history, it came in with #501428: Date and time field type in core. The first time it appeared was in this re-roll by @webchick. I think we're safe to remove it as it isn't documented, but I'll confer with other committers.
nit, sets, not set's
two usings here
not sure we need to break the line here, that's something phpstorm does automatically, but it doesn't help readability, I think it makes it worse
Comment #67
jonathanshawComment #68
catchThis bit doesn't actually do anything with $element['#timezone'] it just assigns $element['#date_timezone'] = $element['#data_timezone'] which is a no-op.
This could maybe have an effect but isn't the whole point we want to remove the fallback handling here?
Seems fine to me to remove those two hunks.
Comment #69
Anonymous (not verified) commented#66, #68: thanks for review!
66.1: Done.
66.2: Done (butterfly effect on other line).
66.3: Done.
+ few other clean-up.
Comment #70
jonathanshawthanks @vaplas!
Comment #73
jonathanshawComment #74
jofitzRe-rolled.
Comment #75
jhedstromThanks @Jo Fitzgerald. Back to RTBC.
Comment #76
plachLooks good to me as well, @larowlan's feedback was addressed and I think it's ok to perform this change in a minor as plugin classes are not considered part of the public API.
Patch doesn't apply cleanly anymore though.
Comment #77
jofitzRe-rolled (deja vu).
Comment #78
jonathanshawthanks Jo
Comment #79
plachSaving credits.
Comment #81
plachCommitted 5731bc7, pushed to 8.7.x, and published the CR. Thanks!
Comment #82
joelpittetTracking down a bug related to this commit and
date_recur. I'll dig in deeper but these$date->setTimezone(new \DateTimeZone($element['#date_timezone']));calls are sometime's DateTimeZone objects and fatal errors occur.I'll try to track down the source
Comment #83
joelpittetFound the source in date_recur #3015647: $element['value']['#date_timezone'] is not a string fatal error.
Comment #84
gambryIt's me being pedantic or due #3015647: $element['value']['#date_timezone'] is not a string fatal error. we should stress '#date_timezone' must be the timezone name/string?
Currently says: "the local timezone" which one may wrongly assume DateTimeZone object is valid?
Comment #85
plachA follow-up to improve docs would be welcome :)
Comment #86
gambry#3016064: Improve documentation for Datetime and Datelist #date_timezone property created!
Thanks!