[TYPO3-core] RFC: Bug #1697: Datetime input fields and timezone shift bug: REMINDER

Ernesto Baschny [cron IT] ernst at cron-it.de
Mon May 7 20:38:19 CEST 2007


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