[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