[TYPO3-core] RFC: Bug #1697: Datetime input fields and timezone shift bug: REMINDER
Ernesto Baschny [cron IT]
ernst at cron-it.de
Fri Oct 12 23:55:40 CEST 2007
Hi,
well, this is a REMINDER of a "good old known" bug. What a nice
opportunity we have here to get that tested by a big international
audience if we could include that in the first 4.2 alpha! :)
Someone wants to review that? The patch still applies (just some
warnings about offsets in hunks).
I really tried hard to make the description as precise as possible and
even given a step-by-step "walkthrough" the patch. There is an extension
with test-cases (see bug tracker), there is also a 1-2-3 step-by-step
guide to reproduce the bugs and see how they magically disappear when
the patch is applied. What more do you want? :)
Maybe you should just print out the commented code (my mail from
7.5.2007) and read that in bed or somewhere confortable. ;)
Cheers,
Ernesto
Ernesto Baschny [cron IT] wrote: on 07.05.2007 20:38:
> Ernesto Baschny [cron IT] wrote: on 30.04.2007 12:52:
>
>> This is SVN patch request.
>>
>> Problem:
>> Editing date, datetime, time and timesec fields in BE when you have an
>> offset between your clients timezone and the server timezone leads to
>> wrong information being stored.
>>
>> Related to this: The click-checkbox to set the current date did not
>> work on fields with eval year, time and timesec. This is also fixed
>> with the patch.
>
> Ok, this is a reminder, but also a "walk-through" the patch. I
> understand that this patch is hairy to understand without some
> explanations, although I have tried already to give a pretty nice "how
> to reproduce" in my original post.
>
> Please make use of the extension posted in
> http://bugs.typo3.org/view.php?id=1697, which enables you to test all
> possible date fields in a single table.
>
> I guess I have more chances of getting that reviewed if I comment the
> patch changes in place. Please read the original RFC first, then come
> back here.
>
> Some background info: We have the following eval types for "date" type
> of input fields in TCA:
>
> - date (dd.mm.yyyy)
> - datetime (dd.mm.yyyy hh:mm)
> - year (yyyy)
> - time (hh:mm)
> - timesecs (hh:mm:ss)
>
> **************************************************************
>
> First, this is where TYPO3 will render the Backend form (tceforms). For
> date fields that have a "checkbox => '1'" in TCA, TYPO3 will add a
> onClick javascript to set the current date on that checkbox.
>
> Problem #1 with current code: Current time() depends on offset to UTC.
> So we switch to sending gmmktime instead.
>
> Problem #2 with current code: Only "date" and "datetime" gets a default.
> This patch part adds the "year" default. The "time" and "timesecs" will
> be added to the tbe_editor later through a javascript call, because we
> don't want to hardcode the current server timestamp in the output HTML,
> but instead want the checkbox to be evaluated on click (meaning that on
> timesecs I get a new second each second I click on it and not a fixed
> hh:mm:ss in which the html-output was generated).
>
> Index: t3lib/class.t3lib_tceforms.php
> ===================================================================
> --- t3lib/class.t3lib_tceforms.php (revision 2281)
> +++ t3lib/class.t3lib_tceforms.php (working copy)
> @@ -1067,13 +1067,22 @@
> $paramsList =
> "'".$PA['itemFormElName']."','".implode(',',$evalList)."','".trim($config['is_in'])."',".(isset($config['checkbox'])?1:0)..",'".$config['checkbox']."'";
> if (isset($config['checkbox'])) {
> // Setting default "click-checkbox" values for eval types "date"
> and "datetime":
> - $thisMidnight = mktime(0,0,0);
> - $checkSetValue = in_array('date',$evalList) ? $thisMidnight : '';
> - $checkSetValue = in_array('datetime',$evalList) ? time() :
> $checkSetValue;
> -
> + $thisMidnight = gmmktime(0,0,0);
> + if (in_array('date',$evalList)) {
> + $checkSetValue = $thisMidnight;
> + } elseif (in_array('datetime',$evalList)) {
> + $checkSetValue = time();
> + } elseif (in_array('year',$evalList)) {
> + $checkSetValue = gmdate('Y');
> + }
> $cOnClick =
> 'typo3form.fieldGet('.$paramsList.',1,\''.$checkSetValue.'\');'.implode('',$PA['fieldChangeFunc']);
> $item.='<input type="checkbox"'.$this->insertDefStyle('check').'
> name="'.$PA['itemFormElName'].'_cb"
> onclick="'.htmlspecialchars($cOnClick).'" />';
> }
>
>
> **************************************************************
>
> Now this part of the patch will get a potential value coming from the
> database (in case we are editing an existing record). These dates are
> server-TZ-based. This date will be transformed to a UTC timestamp to be
> outputted to the client. This is done by adding the offset of hours from
> our server to UTC, date('O'), which might not even be a integer (there
> are timezones which are at minute :30 when in UTC we have :00).
>
> + if((in_array('date',$evalList) || in_array('datetime',$evalList)) &&
> $PA['itemFormElValue']>0){
> + // Add server timezone offset to UTC to our stored date
> + $hoursOffset = date('O',$PA['itemFormElValue'])/100;
> + $PA['itemFormElValue'] += ($hoursOffset*60*60);
> + }
>
> $PA['fieldChangeFunc'] =
> array_merge(array('typo3form.fieldGet'=>'typo3form.fieldGet('.$paramsList.');'),
> $PA['fieldChangeFunc']);
> $mLgd = ($config['max']?$config['max']:256);
>
>
> **************************************************************
>
> This is now the part which will handle the default-checkbox in cases
> time and timesec, for getting correct seconds through JavaScript. Notice
> that this uses evalFunc_getTimeSecs which will handle the offset to UTC
> calculation.
>
> Index: typo3/jsfunc.tbe_editor.js
> ===================================================================
> --- typo3/jsfunc.tbe_editor.js (revision 2281)
> +++ typo3/jsfunc.tbe_editor.js (working copy)
> @@ -430,6 +430,18 @@
> var theFObj = new evalFunc_dummy (evallist,is_in, checkbox,
> checkboxValue);
> if (checkbox_off) {
> if (document[TBE_EDITOR.formname][theField+"_cb"].checked) {
> + var split = evallist.split(',');
> + for (var i = 0; split.length > i; i++) {
> + var el = split[i].replace(/ /g, '');
> + if (el == 'datetime' || el == 'date') {
> + var now = new Date();
> + checkSetValue = Date.parse(now)/1000 - now.getTimezoneOffset()*60;
> + break;
> + } else if (el == 'time' || el == 'timesec') {
> + checkSetValue = evalFunc_getTimeSecs(new Date());
> + break;
> + }
> + }
> document[TBE_EDITOR.formname][theField].value=checkSetValue;
> } else {
> document[TBE_EDITOR.formname][theField].value=checkboxValue;
>
>
> **************************************************************
>
> Now comes the hairy part of the patch, the evalfield.js, which will
> handle the "client side" of the form handling. We have code which is
> called "onchange":
>
> - evalFunc_input: will check the user-input for correctness and do the
> calculation in cases the user entered something like "+7d" (today + 7
> days). It will transfer the result to the hidden field that holds the
> actual value for this field
>
> - evalFunc_output: this will take the data from the hidden field and
> transfer that to a "human readable" field (with the "_hr" suffix), which
> is what the user will see in the form.
>
> First some new functions are added to ease up calculations based on UTC
> (getTime = Seconds since midnight, getDate = Seconds since epoch,
> 1-1-1970 0:00).
>
> Index: t3lib/jsfunc.evalfield.js
> ===================================================================
> --- t3lib/jsfunc.evalfield.js (revision 2281)
> +++ t3lib/jsfunc.evalfield.js (working copy)
> @@ -34,6 +34,9 @@
> this.getSecs = evalFunc_getSecs;
> this.getYear = evalFunc_getYear;
> this.getTimeSecs = evalFunc_getTimeSecs;
> + this.getTime = evalFunc_getTime;
> + this.getDate = evalFunc_getDate;
> + this.getTimestamp = evalFunc_getTimestamp;
> this.caseSwitch = evalFunc_caseSwitch;
> this.evalObjValue = evalFunc_evalObjValue;
> this.outputObjValue = evalFunc_outputObjValue;
> @@ -44,8 +47,8 @@
> this.btrim = evalFunc_btrim;
> var today = new Date();
> this.lastYear = this.getYear(today);
> - this.lastDate = this.getSecs(today);
> - this.lastTime = this.getTimeSecs(today);
> + this.lastDate = this.getDate(today);
> + this.lastTime = this.getTimestamp(today);
> this.isInString = '';
> this.USmode = 0;
> }
>
> **************************************************************
>
> An obvious typo (missing ;):
>
> @@ -245,7 +248,7 @@
> return this.parseDouble(inVal);
> }
>
> - var today = new Date()
> + var today = new Date();
> var add=0;
> var value = this.ltrim(inVal);
> var values = new evalFunc_split(value);
>
> **************************************************************
>
> We ease up this code, which calculates the timestamp based on
> calculations with "d+7" (today + 7 days) by separating the code which
> calculates what to "add" with later doing that. Note that this is the
> "datetime" part, which will delegate the calculations to the function
> again, separating the "date" from the "time" part in the user input
>
> @@ -261,8 +264,6 @@
> case "d":
> case "t":
> case "n":
> - var theTime = new Date(this.getYear(today), today.getMonth(),
> today.getDate(), today.getHours(), today.getMinutes());
> - this.lastDate = this.getSecs(theTime)
> if (values.valPol[1]) {
> add = this.pol(values.valPol[1],this.parseInt(values.values[1]));
> }
> @@ -276,20 +277,17 @@
> default:
> var index = value.indexOf(' ');
> if (index!=-1) {
> - var theSecs = this.input("date",value.substr(index,value.length))
> + this.input("time",value.substr(0,index));
> - this.lastDate = theSecs;
> + this.lastTime =
> this.input("date",value.substr(index,value.length)) +
> this.input("time",value.substr(0,index));
> }
> }
> - this.lastDate+=add*24*60*60;
> - return this.lastDate;
> + this.lastTime+=add*24*60*60;
> + return this.lastTime;
> break;
> case "year":
> switch (theCmd) {
> case "d":
> case "t":
> case "n":
> - var theTime = today;
> - this.lastYear = this.getYear(theTime);
> if (values.valPol[1]) {
> add = this.pol(values.valPol[1],this.parseInt(values.values[1]));
> }
>
> **************************************************************
>
> Ok, this is just a fix of indenting in the "year" construct. No code was
> harmed during this change:
>
>
> @@ -305,11 +303,13 @@
> add = this.pol(values.valPol[2],this.parseInt(values.values[2]));
> }
> var year =
> (values.values[1])?this.parseInt(values.values[1]):this.getYear(today);
> - if ( (year>=0&&year<38) || (year>=70&&year<100) ||
> (year>=1970&&year<2038) ) {
> - if (year<100) {
> - year = (year<38) ? year+=2000 : year+=1900;
> - }
> - } else {year = this.getYear(today);}
> + if ( (year>=0&&year<38) || (year>=70&&year<100) ||
> (year>=1970&&year<2038) ) {
> + if (year<100) {
> + year = (year<38) ? year+=2000 : year+=1900;
> + }
> + } else {
> + year = this.getYear(today);
> + }
> this.lastYear = year
> }
> this.lastYear+=add;
>
> **************************************************************
>
> This is now the "date" part of the input checking: Instead of now
> checking for the date in our javascript, we simply pass the values over
> to the "new Date()" javascript function. This will already do proper
> checkings and handle the stuff correctly, so we don't need to recreate
> that functionality here again. This is why we have lots of code removed
> in this part.
>
> Input "30-2-2006" will return a date of "2-3-2006" and so on. Only the
> year handling is kept, because we need to be aware to two digit years
> and make them "y2k" compatible while checking them (00-38 = 2000-2038,
> 39-99 = 1939-1999). A new feature here: no year entered defaults to the
> current year.
>
> Also convert the given timestamp back to UTC datetime by substracting
> the offset from the client to UTC at the end (return value will be
> filled into the hidden field which the client will later send back to
> the server).
>
> @@ -320,8 +320,6 @@
> case "d":
> case "t":
> case "n":
> - var theTime = new Date(this.getYear(today), today.getMonth(),
> today.getDate());
> - this.lastDate = this.getSecs(theTime);
> if (values.valPol[1]) {
> add = this.pol(values.valPol[1],this.parseInt(values.values[1]));
> }
> @@ -346,23 +344,22 @@
> }
>
> var year =
> (values.values[3])?this.parseInt(values.values[3]):this.getYear(today);
> - if ( (year>=0&&year<38) || (year>=70&&year<100) ||
> (year>=1970&&year<2038) ) {
> - if (year<100) {
> - year = (year<38) ? year+=2000 : year+=1900;
> - }
> - } else {year = this.getYear(today);}
> - var month =
> (values.values[this.USmode?1:2])?this.parseInt(values.values[this.USmode?1:2]):today.getMonth()+1;
> - if (month > 12) {month=12;}
> - if (month < 1) {month=1;}
> - var day =
> (values.values[this.USmode?2:1])?this.parseInt(values.values[this.USmode?2:1]):today.getDate();
> - if (day > 31) {day=31;}
> - if (day < 1) {day=1;}
> - if (''+day+'-'+month+'-'+year == "1-1-1970") {
> - var theTime = new Date(); theTime.setTime(0);
> + if ( (year>=0&&year<38) || (year>=70&&year<100) ||
> (year>=1970&&year<2038) ) {
> + if (year<100) {
> + year = (year<38) ? year+=2000 : year+=1900;
> + }
> } else {
> - var theTime = new Date(parseInt(year), parseInt(month)-1,
> parseInt(day));
> + year = this.getYear(today);
> }
> - this.lastDate = this.getSecs(theTime)
> + var month =
> (values.values[this.USmode?1:2])?this.parseInt(values.values[this.USmode?1:2]):today.getUTCMonth()+1;
> + var day =
> (values.values[this.USmode?2:1])?this.parseInt(values.values[this.USmode?2:1]):today.getUTCDate();
> +
> + var theTime = new Date(parseInt(year), parseInt(month)-1,
> parseInt(day));
> +
> + // Substract timezone offset from client
> + this.lastDate = this.getTimestamp(theTime);
> + theTime.setTime((this.lastDate -
> theTime.getTimezoneOffset()*60)*1000);
> + this.lastDate = this.getTimestamp(theTime);
> }
> this.lastDate+=add*24*60*60;
> if (this.lastDate<0) {this.lastDate=0;}
>
>
> **************************************************************
>
> Basically this is the same as above, but for the "time" and "timesecs"
> case. We just make the code slimmer and make sure we return timestamps
> based on UTC instead of local-TZ timestamps. So the time "0:00" will
> return a "0" integer and not "3600" if we are in CET or "7200" in case
> of CEST, which is what the code used to do.
>
> @@ -374,8 +371,6 @@
> case "d":
> case "t":
> case "n":
> - var theTime = new Date(this.getYear(today), today.getMonth(),
> today.getDate(), today.getHours(), today.getMinutes(),
> ((type=="timesec")?today.getSeconds():0));
> - this.lastTime = this.getTimeSecs(theTime);
> if (values.valPol[1]) {
> add = this.pol(values.valPol[1],this.parseInt(values.values[1]));
> }
> @@ -398,14 +393,18 @@
> var temp = values.values[1];
> values = new evalFunc_splitSingle(temp);
> }
> - var sec =
> (values.values[3])?this.parseInt(values.values[3]):today.getSeconds();
> - if (sec > 59) {sec=59;}
> - var min =
> (values.values[2])?this.parseInt(values.values[2]):today.getMinutes();
> - if (min > 59) {min=59;}
> - var hour =
> (values.values[1])?this.parseInt(values.values[1]):today.getHours();
> - if (hour > 23) {hour=23;}
> - var theTime = new Date(this.getYear(today), today.getMonth(),
> today.getDate(), hour, min, ((type=="timesec")?sec:0));
> - this.lastTime = this.getTimeSecs(theTime)
> + var sec =
> (values.values[3])?this.parseInt(values.values[3]):today.getUTCSeconds();
> + if (sec > 59) {sec=59;}
> + var min =
> (values.values[2])?this.parseInt(values.values[2]):today.getUTCMinutes();
> + if (min > 59) {min=59;}
> + var hour =
> (values.values[1])?this.parseInt(values.values[1]):today.getUTCHours();
> + if (hour > 23) {hour=23;}
> +
> + var theTime = new Date(this.getYear(today), today.getUTCMonth(),
> today.getUTCDate(), hour, min, ((type=="timesec")?sec:0));
> +
> + this.lastTime = this.getTimestamp(theTime);
> + theTime.setTime((this.lastTime -
> theTime.getTimezoneOffset()*60)*1000);
> + this.lastTime = this.getTime(theTime);
> }
> this.lastTime+=add*60;
> if (this.lastTime<0) {this.lastTime+=24*60*60;}
>
>
> **************************************************************
>
> Now we are in the evalFunc_output() part, which will put the value from
> the hidden field back into the "_hr" (human readable) field that will be
> displayed in the users client. As we have a UTC-based timestamp in that
> hidden field, we have to make sure we handle that as such, and use
> getUTC* javascript methods for converting that to a human readable date.
>
> @@ -420,28 +419,24 @@
> switch (type) {
> case "date":
> if (!parseInt(value)) {return '';}
> - var theTime = new Date();
> - theTime.setTime(value*1000);
> + var theTime = new Date(parseInt(value) * 1000);
> if (this.USmode) {
> - theString =
> (theTime.getMonth()+1)+'-'+theTime.getDate()+'-'+this.getYear(theTime);
> + theString =
> (theTime.getUTCMonth()+1)+'-'+theTime.getUTCDate()+'-'+this.getYear(theTime);
> } else {
> - theString =
> theTime.getDate()+'-'+(theTime.getMonth()+1)+'-'+this.getYear(theTime);
> + theString =
> theTime.getUTCDate()+'-'+(theTime.getUTCMonth()+1)+'-'+this.getYear(theTime);
> }
> break;
> case "datetime":
> if (!parseInt(value)) {return '';}
> - var theTime = new Date();
> - theTime.setTime(value*1000);
> - theString = this.output("time",this.getTimeSecs(theTime))+'
> '+this.output("date",value);
> + theString = this.output("time",value)+' '+this.output("date",value);
> break;
> case "time":
> case "timesec":
> if (!parseInt(value)) {return '';}
> - var theTime = new Date();
> - theTime.setTime(value*1000);
> - var h = Math.floor(value/3600);
> - var m = Math.floor((value-h*3600)/60);
> - var s = Math.floor(value-h*3600-m*60);
> + var theTime = new Date(parseInt(value) * 1000);
> + var h = theTime.getUTCHours();
> + var m = theTime.getUTCMinutes();
> + var s = theTime.getUTCSeconds();
> theString = h+':'+((m<10)?'0':'')+m +
> ((type=="timesec")?':'+((s<10)?'0':'')+s:'');
> break;
> case "password":
>
>
> **************************************************************
>
> At last, we have the methods that we use at some places that do the
> calculation of timestamps for a Date object in javascript:
>
> @@ -456,14 +451,23 @@
> return theString;
> }
> function evalFunc_getSecs(timeObj) {
> - return Math.round(timeObj.getTime()/1000);
> + return Math.round(timeObj.getUTCSeconds()/1000);
> }
> +// Seconds since midnight:
> +function evalFunc_getTime(timeObj) {
> + return
> timeObj.getUTCHours()*60*60+timeObj.getUTCMinutes()*60+Math.round(timeObj.getUTCSeconds()/1000);
> +}
> function evalFunc_getYear(timeObj) {
> - return (timeObj.getYear()>200) ? timeObj.getYear() :
> (timeObj.getYear()+1900);
> + return timeObj.getUTCFullYear();
> }
> +// Seconds since midnight with client timezone offset:
> function evalFunc_getTimeSecs(timeObj) {
> return
> timeObj.getHours()*60*60+timeObj.getMinutes()*60+timeObj.getSeconds();
> }
> +function evalFunc_getDate(timeObj) {
> + var theTime = new Date(this.getYear(timeObj), timeObj.getUTCMonth(),
> timeObj.getUTCDate());
> + return this.getTimestamp(theTime);
> +}
> function evalFunc_dummy (evallist,is_in,checkbox,checkboxValue) {
> this.evallist = evallist;
> this.is_in = is_in;
> @@ -483,4 +487,6 @@
> if(ePos == -1) {ePos = theStr.length;}
> return (theStr.substring(sPos+lengthOfDelim,ePos));
> }
> -
> +function evalFunc_getTimestamp(timeObj) {
> + return Date.parse(timeObj)/1000;
> +}
>
>
> **************************************************************
>
> At last, when submitting such a form back to the server, we will get a
> UTC-based timestamp for sure. A time/timesec timestamp is already
> UTC-based in our current TYPO3 database (meaning 0:00 is "0"). We just
> need to go back to the server timezone in the date/datetime situations,
> which means substracting the offset from the server TZ to UTC.
>
> Index: t3lib/class.t3lib_tcemain.php
> ===================================================================
> --- t3lib/class.t3lib_tcemain.php (revision 2281)
> +++ t3lib/class.t3lib_tcemain.php (working copy)
> @@ -1921,12 +1921,18 @@
> switch($func) {
> case 'int':
> case 'year':
> - case 'date':
> - case 'datetime':
> case 'time':
> case 'timesec':
> $value = intval($value);
> break;
> + case 'date':
> + case 'datetime':
> + $value = intval($value);
> + if($value>0){
> + $zone = date('O',$value)/100;
> + $value -= ($zone*60*60);
> + }
> + break;
> case 'double2':
> $theDec = 0;
> for ($a=strlen($value); $a>0; $a--) {
>
>
>
> Understood now? ;)
>
> Cheers,
> Ernesto
More information about the TYPO3-team-core
mailing list