[BUGFIX] Make date/time fields working again 42/51242/16
authorMarkus Klein <markus.klein@typo3.org>
Wed, 7 Jun 2017 13:47:46 +0000 (15:47 +0200)
committerChristian Kuhn <lolli@schwarzbu.ch>
Fri, 9 Jun 2017 13:28:17 +0000 (15:28 +0200)
This patch solves an issue with time-fields and a couple of
inconsistencies for time(sec), date and datetime fields in BE:

- BackendUtility::time() is superfluous as gmdate() can be used
- Ensure stored data is identical to TYPO3 7
- Only use ISO dates in UTC timezone to communicate with JS
  (time fields use 1970-01-01 as date)
- Only write ISO dates in UTC back to hidden FormEngine fields
- Do that for all date/time fields to simplify JS code
- Take care of necessary timezone conversion for date(time) fields
- Correctly handle manual input
- Fix record titles if a date field is the label field
- Fix wrong Acceptance tests
- Fix wrong Unit tests
- Remove superfluous data-date-offset attribute for DateTimePicker
- Streamline FormEngineValidation date handling code
- Refactor DateTimePicker: fix code duplication, work in UTC only

Resolves: #79249
Releases: master, 8.7
Change-Id: I12ef6b6f59f0843182e093f8f274989161820894
Reviewed-on: https://review.typo3.org/51242
Tested-by: TYPO3com <no-reply@typo3.com>
Reviewed-by: Johannes Kasberger <johannes.kasberger@reelworx.at>
Tested-by: Johannes Kasberger <johannes.kasberger@reelworx.at>
Reviewed-by: Christian Kuhn <lolli@schwarzbu.ch>
Tested-by: Christian Kuhn <lolli@schwarzbu.ch>
14 files changed:
typo3/sysext/backend/Classes/Form/Element/AbstractFormElement.php
typo3/sysext/backend/Classes/Form/Element/InputDateTimeElement.php
typo3/sysext/backend/Classes/Form/FormDataProvider/TcaRecordTitle.php
typo3/sysext/backend/Classes/Utility/BackendUtility.php
typo3/sysext/backend/Resources/Public/JavaScript/DateTimePicker.js
typo3/sysext/backend/Resources/Public/JavaScript/FormEngineValidation.js
typo3/sysext/backend/Tests/JavaScript/FormEngineValidationTest.js
typo3/sysext/backend/Tests/Unit/Form/Element/AbstractFormElementTest.php
typo3/sysext/belog/Resources/Private/Partials/Content/Filter.html
typo3/sysext/core/Classes/DataHandling/DataHandler.php
typo3/sysext/core/Classes/Database/QueryGenerator.php
typo3/sysext/core/Tests/Acceptance/Backend/Formhandler/ElementsBasicInputDateCest.php
typo3/sysext/core/Tests/Unit/DataHandling/DataHandlerTest.php
typo3/sysext/scheduler/Classes/Controller/SchedulerModuleController.php

index a364faa..14cca66 100644 (file)
@@ -225,13 +225,13 @@ abstract class AbstractFormElement extends AbstractNode
             case 'time':
                 // compatibility with "eval" (type "input")
                 if ($itemValue !== '' && !is_null($itemValue)) {
-                    $itemValue = date('H:i', (int)$itemValue);
+                    $itemValue = gmdate('H:i', (int)$itemValue);
                 }
                 break;
             case 'timesec':
                 // compatibility with "eval" (type "input")
                 if ($itemValue !== '' && !is_null($itemValue)) {
-                    $itemValue = date('H:i:s', (int)$itemValue);
+                    $itemValue = gmdate('H:i:s', (int)$itemValue);
                 }
                 break;
             case 'year':
index ea1540a..4b1afdd 100644 (file)
@@ -135,14 +135,17 @@ class InputDateTimeElement extends AbstractFormElement
         }
 
         if ($format === 'datetime' || $format === 'date') {
-            // convert timestamp to proper ISO-8601 date so we get rid of timezone issues on the client.
-            // This only handles integer timestamps; if the field is a date(time), it already was converted to an
-            // ISO-8601 date by DatabaseRowDateTimeFields.
+            // This only handles integer timestamps; if the field is a SQL native date(time), it was already converted
+            // to an ISO-8601 date by the DatabaseRowDateTimeFields class. (those dates are stored as server local time)
             if (MathUtility::canBeInterpretedAsInteger($itemValue) && $itemValue != 0) {
-                // output date as a ISO-8601 date; the stored value is the server time zone, so we need to treat it as such.
-                $timestamp = $itemValue;
-                $timestamp += date('Z', $timestamp);
-                $itemValue = gmdate('c', $timestamp);
+                // We store UTC timestamps in the database.
+                // Convert the timestamp to a proper ISO-8601 date so we get rid of timezone issues on the client.
+                // Details: As the JS side is not capable of handling dates in the server's timezone
+                // (moment.js can only handle UTC or browser's local timezone), we need to offset the value
+                // to eliminate the timezone. JS will receive all dates as if they were UTC, which we undo on save in DataHandler
+                $adjustedValue = $itemValue + date('Z', (int)$itemValue);
+                // output date as a ISO-8601 date
+                $itemValue = gmdate('c', $adjustedValue);
             }
             if (isset($config['range']['lower'])) {
                 $attributes['data-date-minDate'] = (int)$config['range']['lower'];
@@ -151,6 +154,11 @@ class InputDateTimeElement extends AbstractFormElement
                 $attributes['data-date-maxDate'] = (int)$config['range']['upper'];
             }
         }
+        if (($format === 'time' || $format === 'timesec') && MathUtility::canBeInterpretedAsInteger($itemValue) && $itemValue != 0) {
+            // time(sec) is stored as elapsed seconds in DB, hence we interpret it as UTC time on 1970-01-01
+            // and pass on the ISO format to JS.
+            $itemValue = gmdate('c', (int)$itemValue);
+        }
 
         $fieldInformationResult = $this->renderFieldInformation();
         $fieldInformationHtml = $fieldInformationResult['html'];
index 4f95ef6..c974132 100644 (file)
@@ -332,11 +332,11 @@ class TcaRecordTitle implements FormDataProviderInterface
             }
         } elseif (GeneralUtility::inList($fieldConfig['eval'], 'time')) {
             if (!empty($value)) {
-                $title = BackendUtility::time((int)$value, false);
+                $title = gmdate('H:i', (int)$value);
             }
         } elseif (GeneralUtility::inList($fieldConfig['eval'], 'timesec')) {
             if (!empty($value)) {
-                $title = BackendUtility::time((int)$value);
+                $title = gmdate('H:i:s', (int)$value);
             }
         } elseif (GeneralUtility::inList($fieldConfig['eval'], 'datetime')) {
             // Handle native date/time field
index 99e90d8..d66b76a 100644 (file)
@@ -1106,14 +1106,7 @@ class BackendUtility
      */
     public static function time($value, $withSeconds = true)
     {
-        $hh = floor($value / 3600);
-        $min = floor(($value - $hh * 3600) / 60);
-        $sec = $value - $hh * 3600 - $min * 60;
-        $l = sprintf('%02d', $hh) . ':' . sprintf('%02d', $min);
-        if ($withSeconds) {
-            $l .= ':' . sprintf('%02d', $sec);
-        }
-        return $l;
+        return gmdate('H:i' . ($withSeconds ? ':s' : ''), (int)$value);
     }
 
     /**
@@ -2155,11 +2148,11 @@ class BackendUtility
                         }
                     } elseif (GeneralUtility::inList($theColConf['eval'], 'time')) {
                         if (!empty($value)) {
-                            $l = self::time($value, false);
+                            $l = gmdate('H:i', (int)$value);
                         }
                     } elseif (GeneralUtility::inList($theColConf['eval'], 'timesec')) {
                         if (!empty($value)) {
-                            $l = self::time($value);
+                            $l = gmdate('H:i:s', (int)$value);
                         }
                     } elseif (GeneralUtility::inList($theColConf['eval'], 'datetime')) {
                         // Handle native date/time field
index 1a053b1..4438380 100644 (file)
  * and EXT:belog and EXT:scheduler
  */
 define(['jquery'], function($) {
+       "use strict";
 
        /**
-        *
         * @type {{options: {fieldSelector: string, format: *}}}
         * @exports TYPO3/CMS/Backend/DateTimePicker
         */
        var DateTimePicker = {
                options: {
                        fieldSelector: '.t3js-datetimepicker',
-                       format: (opener != null && typeof opener.top.TYPO3 !== 'undefined' ? opener.top : top).TYPO3.settings.DateTimePicker.DateFormat
+                       format: (opener !== null && typeof opener.top.TYPO3 !== 'undefined' ? opener.top : top).TYPO3.settings.DateTimePicker.DateFormat
                }
        };
 
@@ -38,147 +38,131 @@ define(['jquery'], function($) {
        DateTimePicker.initialize = function() {
                // fetch the date time fields that haven't been initialized yet
                var $dateTimeFields = $(DateTimePicker.options.fieldSelector).filter(function() {
-                       return ($(this).data('DateTimePicker') == undefined);
+                       return $(this).data('DateTimePicker') === undefined;
                });
 
                if ($dateTimeFields.length > 0) {
                        require(['moment', 'TYPO3/CMS/Backend/Storage', 'twbs/bootstrap-datetimepicker'], function(moment, Storage) {
                                var userLocale = Storage.Persistent.get('lang');
-                               var setLocale = false;
-                               if (userLocale) {
-                                       setLocale = moment.locale(userLocale);
-                               }
+                               var setLocale = userLocale ? moment.locale(userLocale) : false;
 
                                // initialize the datepicker on each selected element
                                $dateTimeFields.each(function() {
-                                       var $element = $(this);
-                                       var format = DateTimePicker.options.format;
-                                       var type = $element.data('dateType');
-                                       var options = {
-                                               sideBySide: true,
-                                               icons: {
-                                                       time: 'fa fa-clock-o',
-                                                       date: 'fa fa-calendar',
-                                                       up: 'fa fa-chevron-up',
-                                                       down: 'fa fa-chevron-down',
-                                                       previous: 'fa fa-chevron-left',
-                                                       next: 'fa fa-chevron-right',
-                                                       today: 'fa fa-calendar-o',
-                                                       clear: 'fa fa-trash'
-                                               }
-                                       };
-
-                                       // set options based on type
-                                       switch (type) {
-                                               case 'datetime':
-                                                       options.format = format[1];
-                                                       break;
-                                               case 'date':
-                                                       options.format = format[0];
-                                                       break;
-                                               case 'time':
-                                                       options.format = 'HH:mm';
-                                                       break;
-                                               case 'timesec':
-                                                       options.format = 'HH:mm:ss';
-                                                       break;
-                                               case 'year':
-                                                       options.format = 'YYYY';
-                                                       break;
-                                       }
-
-                                       // datepicker expects the min and max dates to be formatted with options.format but unix timestamp given
-                                       if ($element.data('dateMindate')) {
-                                               $element.data('dateMindate', moment.unix($element.data('dateMindate')).format(options.format));
-                                       }
-                                       if ($element.data('dateMaxdate')) {
-                                               $element.data('dateMaxdate', moment.unix($element.data('dateMaxdate')).format(options.format));
-                                       }
-
-                                       if (setLocale) {
-                                               options.locale = setLocale;
-                                       }
-
-                                       // initialize the date time picker on this element
-                                       $element.datetimepicker(options);
+                                       DateTimePicker.initializeField(moment, $(this), setLocale);
                                });
 
-                               $dateTimeFields.on('blur', function(event) {
+                               $dateTimeFields.on('blur', function() {
                                        var $element = $(this);
                                        var $hiddenField = $element.parent().parent().find('input[type=hidden]');
 
                                        if ($element.val() === '') {
                                                $hiddenField.val('');
                                        } else {
+                                               var type = $element.data('dateType');
                                                var format = $element.data('DateTimePicker').format();
                                                var date = moment.utc($element.val(), format);
-                                               var calculateTimeZoneOffset = $element.data('date-offset');
-                                               var timeZoneOffset;
-
-                                               if (typeof calculateTimeZoneOffset !== 'undefined') {
-                                                       timeZoneOffset = parseInt(calculateTimeZoneOffset);
-                                               } else {
-                                                       timeZoneOffset = date.utcOffset() * 60 * -1;
-                                               }
-
                                                if (date.isValid()) {
-                                                       var value;
-                                                       // Format the value for the hidden field that is passed on to the backend, i.e. most likely DataHandler.
-                                                       // The format for that is the timestamp for time fields, and a full-blown ISO-8601 timestamp for all
-                                                       // date-related fields.
-                                                       switch ($element.data('dateType')) {
-                                                               case 'time':
-                                                                       value = date.format('HH:mm');
-                                                                       break;
-                                                               case 'timesec':
-                                                                       value = date.format('HH:mm:ss');
-                                                                       break;
-                                                               default:
-                                                                       value = date.format();
-                                                       }
-                                                       $hiddenField.val(value);
+                                                       $hiddenField.val(DateTimePicker.formatDateForHiddenField(date, type));
                                                } else {
-                                                       $element.val(moment($hiddenField.val()).utc().format(format));
+                                                       $element.val(DateTimePicker.formatDateForHiddenField(moment.utc($hiddenField.val()), type));
                                                }
                                        }
                                });
 
                                // on datepicker change, write the selected date with the timezone offset to the hidden field
-                               $dateTimeFields.on('dp.change', function(event) {
+                               $dateTimeFields.on('dp.change', function(evt) {
                                        var $element = $(this);
                                        var $hiddenField = $element.parent().parent().find('input[type=hidden]');
+                                       var type = $element.data('dateType');
+                                       var value = '';
 
-                                       if ($element.val() === '') {
-                                               $hiddenField.val('');
-                                       } else {
-                                               var date = event.date.utc();
-
-                                               var calculateTimeZoneOffset = $element.data('date-offset');
-                                               var timeZoneOffset, value;
-                                               if (typeof calculateTimeZoneOffset !== 'undefined') {
-                                                       timeZoneOffset = parseInt(calculateTimeZoneOffset);
-                                               } else {
-                                                       timeZoneOffset = date.utcOffset() * 60 * -1;
-                                               }
-
-                                               switch ($element.data('dateType')) {
-                                                       case 'time':
-                                                               value = date.format('HH:mm');
-                                                               break;
-                                                       case 'timesec':
-                                                               value = date.format('HH:mm:ss');
-                                                               break;
-                                                       default:
-                                                               value = date.format();
-                                               }
-                                               $hiddenField.val(value);
+                                       if ($element.val() !== '') {
+                                               value = DateTimePicker.formatDateForHiddenField(evt.date.utc(), type);
                                        }
+                                       $hiddenField.val(value);
+
                                        $(document).trigger('formengine.dp.change', [$(this)]);
                                });
                        });
                }
        };
 
-       DateTimePicker.initialize();
+       /**
+        * Initialize a single field
+        *
+        * @param {moment} moment
+        * @param {object} $element
+        * @param {string} locale
+        */
+       DateTimePicker.initializeField = function(moment, $element, locale) {
+               var format = DateTimePicker.options.format;
+               var type = $element.data('dateType');
+               var options = {
+                       sideBySide: true,
+                       icons: {
+                               time: 'fa fa-clock-o',
+                               date: 'fa fa-calendar',
+                               up: 'fa fa-chevron-up',
+                               down: 'fa fa-chevron-down',
+                               previous: 'fa fa-chevron-left',
+                               next: 'fa fa-chevron-right',
+                               today: 'fa fa-calendar-o',
+                               clear: 'fa fa-trash'
+                       }
+               };
+
+               // set options based on type
+               switch (type) {
+                       case 'datetime':
+                               options.format = format[1];
+                               break;
+                       case 'date':
+                               options.format = format[0];
+                               break;
+                       case 'time':
+                               options.format = 'HH:mm';
+                               break;
+                       case 'timesec':
+                               options.format = 'HH:mm:ss';
+                               break;
+                       case 'year':
+                               options.format = 'YYYY';
+                               break;
+               }
+
+               // datepicker expects the min and max dates to be formatted with options.format but unix timestamp given
+               if ($element.data('dateMindate')) {
+                       $element.data('dateMindate', moment.unix($element.data('dateMindate')).format(options.format));
+               }
+               if ($element.data('dateMaxdate')) {
+                       $element.data('dateMaxdate', moment.unix($element.data('dateMaxdate')).format(options.format));
+               }
+
+               if (locale) {
+                       options.locale = locale;
+               }
+
+               // initialize the date time picker on this element
+               $element.datetimepicker(options);
+       };
+
+       /**
+        * Format a given date for the hidden FormEngine field
+        *
+        * Format the value for the hidden field that is passed on to the backend, i.e. most likely DataHandler.
+        * The format for that is the timestamp for time fields, and a full-blown ISO-8601 timestamp for all date-related fields.
+        *
+        * @param {moment} date
+        * @param {string} type Type of the date
+        * @returns {string}
+        */
+       DateTimePicker.formatDateForHiddenField = function(date, type) {
+               if (type === 'time' || type === 'timesec') {
+                       date.year(1970).month(0).date(1);
+               }
+               return date.format();
+       };
+
+       $(DateTimePicker.initialize);
        return DateTimePicker;
 });
index 13810fd..628fe04 100644 (file)
@@ -21,7 +21,7 @@ define(['jquery', 'moment'], function ($, moment) {
        /**
         * The main FormEngineValidation object
         *
-        * @type {{rulesSelector: string, inputSelector: string, markerSelector: string, groupFieldHiddenElement: string, relatedFieldSelector: string, errorClass: string, lastYear: number, lastDate: number, lastTime: number, refDate: Date, USmode: number, passwordDummy: string}}
+        * @type {{rulesSelector: string, inputSelector: string, markerSelector: string, groupFieldHiddenElement: string, relatedFieldSelector: string, errorClass: string, lastYear: number, lastDate: number, lastTime: number, USmode: number, passwordDummy: string}}
         * @exports TYPO3/CMS/Backend/FormEngineValidation
         */
        var FormEngineValidation = {
@@ -34,7 +34,6 @@ define(['jquery', 'moment'], function ($, moment) {
                lastYear: 0,
                lastDate: 0,
                lastTime: 0,
-               refDate: new Date(),
                USmode: 0,
                passwordDummy: '********'
        };
@@ -58,7 +57,6 @@ define(['jquery', 'moment'], function ($, moment) {
                FormEngineValidation.lastYear = FormEngineValidation.getYear(today);
                FormEngineValidation.lastDate = FormEngineValidation.getDate(today);
                FormEngineValidation.lastTime = 0;
-               FormEngineValidation.refDate = today;
                FormEngineValidation.USmode = 0;
                FormEngineValidation.validate();
        };
@@ -110,10 +108,10 @@ define(['jquery', 'moment'], function ($, moment) {
                        var value = $field.val();
 
                        for (var i = 0; i < evalList.length; i++) {
-                               value = FormEngineValidation.formatValue(evalList[i], value, config)
+                               value = FormEngineValidation.formatValue(evalList[i], value, config);
                        }
                        // Prevent password fields to be overwritten with original value
-                       if (value.length && $humanReadableField.attr('type') != 'password') {
+                       if (value.length && $humanReadableField.attr('type') !== 'password') {
                                $humanReadableField.val(value);
                        }
                }
@@ -138,18 +136,19 @@ define(['jquery', 'moment'], function ($, moment) {
         */
        FormEngineValidation.formatValue = function(type, value, config) {
                var theString = '';
+               var parsedInt, theTime;
                switch (type) {
                        case 'date':
                                // poor man’s ISO-8601 detection: if we have a "-" in it, it apparently is not an integer.
                                if (value.toString().indexOf('-') > 0) {
-                                       var date = moment(value).utc();
+                                       var date = moment.utc(value);
                                        if (FormEngineValidation.USmode) {
                                                theString = date.format('MM-DD-YYYY');
                                        } else {
                                                theString = date.format('DD-MM-YYYY');
                                        }
                                } else {
-                                       var parsedInt = parseInt(value);
+                                       parsedInt = parseInt(value);
                                        if (!parsedInt) {
                                                return '';
                                        }
@@ -169,23 +168,20 @@ define(['jquery', 'moment'], function ($, moment) {
                                break;
                        case 'time':
                        case 'timesec':
+                               var dateValue;
                                if (value.toString().indexOf('-') > 0) {
-                                       var date = moment(value).utc();
-                                       if (type == 'timesec') {
-                                               theString = date.format('HH:mm:ss');
-                                       } else {
-                                               theString = date.format('HH:mm');
-                                       }
+                                       dateValue = moment.utc(value);
                                } else {
-                                       var parsedInt = parseInt(value);
+                                       parsedInt = parseInt(value);
                                        if (!parsedInt && value.toString() !== '0') {
                                                return '';
                                        }
-                                       var theTime = new Date(parsedInt * 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 : '');
+                                       dateValue = moment.unix(parsedInt).utc();
+                               }
+                               if (type === 'timesec') {
+                                       theString = dateValue.format('HH:mm:ss');
+                               } else {
+                                       theString = dateValue.format('HH:mm');
                                }
                                break;
                        case 'password':
@@ -655,7 +651,7 @@ define(['jquery', 'moment'], function ($, moment) {
                                break;
                        case '+':
                        case '-':
-                               if (FormEngineValidation.lastTime == 0) {
+                               if (FormEngineValidation.lastTime === 0) {
                                        FormEngineValidation.lastTime = FormEngineValidation.convertClientTimestampToUTC(FormEngineValidation.getTimestamp(today), 0);
                                }
                                if (values.valPol[1]) {
@@ -664,11 +660,9 @@ define(['jquery', 'moment'], function ($, moment) {
                                break;
                        default:
                                var index = value.indexOf(' ');
-                               if (index != -1) {
+                               if (index !== -1) {
                                        var dateVal = FormEngineValidation.parseDate(value.substr(index, value.length), value.substr(0, 1));
-                                       // set refDate so that evalFunc_input on time will work with correct DST information
-                                       FormEngineValidation.refDate = new Date(dateVal * 1000);
-                                       FormEngineValidation.lastTime = dateVal + FormEngineValidation.parseTime(value.substr(0,index), value.substr(0, 1), 'time');
+                                       FormEngineValidation.lastTime = dateVal + FormEngineValidation.parseTime(value.substr(0, index), value.substr(0, 1), 'time');
                                } else {
                                        // only date, no time
                                        FormEngineValidation.lastTime = FormEngineValidation.parseDate(value, value.substr(0, 1));
@@ -730,10 +724,11 @@ define(['jquery', 'moment'], function ($, moment) {
                                usMode = FormEngineValidation.USmode ? 2 : 1;
                                var day = (values.values[usMode]) ? FormEngineValidation.parseInt(values.values[usMode]) : today.getUTCDate();
 
-                               var theTime = new Date(parseInt(year), parseInt(month)-1, parseInt(day));
 
-                               // Substract timezone offset from client
-                               FormEngineValidation.lastDate = FormEngineValidation.convertClientTimestampToUTC(FormEngineValidation.getTimestamp(theTime), 0);
+                               var theTime = moment.utc();
+                               theTime.year(parseInt(year)).month(parseInt(month)-1).date(parseInt(day)).hour(0).minute(0).second(0);
+
+                               FormEngineValidation.lastDate = theTime.unix();
                }
                FormEngineValidation.lastDate += add * 24 * 60 * 60;
                return FormEngineValidation.lastDate;
@@ -770,7 +765,7 @@ define(['jquery', 'moment'], function ($, moment) {
                                }
                                break;
                        default:
-                               var index = (type == 'timesec') ? 4 : 3;
+                               var index = (type === 'timesec') ? 4 : 3;
                                if (values.valPol[index]) {
                                        add = FormEngineValidation.pol(values.valPol[index], FormEngineValidation.parseInt(values.values[index]));
                                }
@@ -794,10 +789,10 @@ define(['jquery', 'moment'], function ($, moment) {
                                        hour = 0;
                                }
 
-                               var theTime = new Date(FormEngineValidation.getYear(FormEngineValidation.refDate), FormEngineValidation.refDate.getUTCMonth(), FormEngineValidation.refDate.getUTCDate(), hour, min, (( type == 'timesec' ) ? sec : 0));
+                               var theTime = moment.utc();
+                               theTime.year(1970).month(0).date(1).hour(hour).minute(min).second(type === 'timesec' ? sec : 0);
 
-                               // Substract timezone offset from client
-                               FormEngineValidation.lastTime = FormEngineValidation.convertClientTimestampToUTC(FormEngineValidation.getTimestamp(theTime), 1);
+                               FormEngineValidation.lastTime = theTime.unix();
                }
                FormEngineValidation.lastTime += add * 60;
                if (FormEngineValidation.lastTime < 0) {
index 3bbd9f1..c76186c 100644 (file)
@@ -67,7 +67,7 @@ define(['jquery', 'TYPO3/CMS/Backend/FormEngineValidation'], function($, FormEng
                                'type': 'time',
                                'value': 0,
                                'config': [],
-                               'result': '0:00'
+                               'result': '00:00'
                        },
                        {
                                'description': 'works for type time with timestamp',
index 16437bd..f9e9cd8 100644 (file)
@@ -101,7 +101,7 @@ class AbstractFormElementTest extends \TYPO3\TestingFramework\Core\Unit\UnitTest
                 [
                     'format' => 'time',
                 ],
-                '1412358894',
+                '64440',
                 '17:54'
             ],
             'format to time with empty value' => [
@@ -122,7 +122,7 @@ class AbstractFormElementTest extends \TYPO3\TestingFramework\Core\Unit\UnitTest
                 [
                     'format' => 'timesec',
                 ],
-                '1412358894',
+                '64494',
                 '17:54:54'
             ],
             'format to timesec with empty value' => [
index 8bd5e80..9e2de80 100644 (file)
                                                value="{f:if(condition: constraint.startTimestamp, then: \"{f:format.date(format:'{settings.timeFormat} {settings.dateFormat}', date: '@{constraint.startTimestamp}')}\")}"
                                                id="manualDateStart"
                                                class="form-control input-sm t3js-datetimepicker t3js-clearable"
-                                               data="{date-type: 'datetime', date-offset: 0}"
+                                               data="{date-type: 'datetime'}"
                                                />
                                <f:form.hidden property="startTimestamp" value="{constraint.startTimestamp}" />
                                <span class="input-group-btn">
                                                value="{f:format.date(format:'{settings.timeFormat} {settings.dateFormat}', date: '@{constraint.endTimestamp}')}"
                                                id="manualDateStop"
                                                class="form-control input-sm t3js-datetimepicker t3js-clearable"
-                                               data="{date-type: 'datetime', date-offset: 0}"
+                                               data="{date-type: 'datetime'}"
                                                />
                                <f:form.hidden property="endTimestamp" />
                                <span class="input-group-btn">
index c2ffc06..9663315 100644 (file)
@@ -1790,7 +1790,7 @@ class DataHandler
         // Handle native date/time fields
         if ($isDateOrDateTimeField) {
             // Convert the timestamp back to a date/time
-            $res['value'] = $res['value'] ? date($format, $res['value']) : $emptyValue;
+            $res['value'] = $res['value'] ? gmdate($format, $res['value']) : $emptyValue;
         }
         return $res;
     }
@@ -2706,23 +2706,32 @@ class DataHandler
                     break;
                 case 'time':
                 case 'timesec':
+                    // If $value is a pure integer we have the number of seconds, we can store that directly
+                    if ($value !== '' && !MathUtility::canBeInterpretedAsInteger($value)) {
+                        // $value is an ISO 8601 date
+                        $value = (new \DateTime($value))->getTimestamp();
+                    }
+                    break;
                 case 'date':
                 case 'datetime':
-                    // a hyphen as first character indicates a negative timestamp
-                    if ((strpos($value, '-') === false && strpos($value, ':') === false) || strpos($value, '-') === 0) {
-                        $value = (int)$value;
-                    } else {
-                        // ISO 8601 dates
-                        $dateTime = new \DateTime($value);
-                        // The returned timestamp is always UTC
-                        $value = $dateTime->getTimestamp();
-                    }
-                    // $value is a UTC timestamp here.
-                    // The value will be stored in the server’s local timezone, but treated as UTC, so we brute force
-                    // subtract the offset here. The offset is subtracted instead of added because the value is stored
-                    // in the timezone, but interpreted as UTC, so if we switched the server to UTC, the correct
-                    // value would be returned.
-                    if ($value !== 0 && !$this->dontProcessTransformations) {
+                    // If $value is a pure integer we have the number of seconds, we can store that directly
+                    if ($value !== null && $value !== '' && !MathUtility::canBeInterpretedAsInteger($value)) {
+                        // The value we receive from JS is an ISO 8601 date, which is always in UTC. (the JS code works like that, on purpose!)
+                        // For instance "1999-11-11T11:11:11Z"
+                        // Since the user actually specifies the time in the server's local time, we need to mangle this
+                        // to reflect the server TZ. So we make this 1999-11-11T11:11:11+0200 (assuming Europe/Vienna here)
+                        // In the database we store the date in UTC (1999-11-11T09:11:11Z), hence we take the timestamp of this converted value.
+                        // For achieving this we work with timestamps only (which are UTC) and simply adjust it for the
+                        // TZ difference.
+                        try {
+                            // Make the date from JS a timestamp
+                            $value = (new \DateTime($value))->getTimestamp();
+                        } catch (\Exception $e) {
+                            // set the default timezone value to achieve the value of 0 as a result
+                            $value = (int)date('Z', 0);
+                        }
+
+                        // @todo this hacky part is problematic when it comes to times around DST switch! Add test to prove that this is broken.
                         $value -= date('Z', $value);
                     }
                     break;
index f4a87c5..30eac16 100644 (file)
@@ -1665,7 +1665,7 @@ class QueryGenerator
         $id = StringUtility::getUniqueId('dt_');
         $html = [];
         $html[] = '<div class="input-group" id="' . $id . '-wrapper">';
-        $html[] = '            <input data-formengine-input-name="' . htmlspecialchars($name) . '" value="' . $value . '" class="form-control t3js-datetimepicker t3js-clearable" data-date-type="' . htmlspecialchars($type) . '" data-date-offset="0" type="text" id="' . $id . '">';
+        $html[] = '            <input data-formengine-input-name="' . htmlspecialchars($name) . '" value="' . $value . '" class="form-control t3js-datetimepicker t3js-clearable" data-date-type="' . htmlspecialchars($type) . '" type="text" id="' . $id . '">';
         $html[] = '            <input name="' . htmlspecialchars($name) . '" value="' . (int)$timestamp . '" type="hidden">';
         $html[] = '            <span class="input-group-btn">';
         $html[] = '                    <label class="btn btn-default" for="' . $id . '">';
index a95156c..f041da6 100644 (file)
@@ -139,36 +139,36 @@ class ElementsBasicInputDateCest extends AbstractElementsBasicCest
                 [
                     'inputValue' => '13:30',
                     'expectedValue' => '13:30',
-                    'expectedInternalValue' => '13:30',
-                    'expectedValueAfterSave' => (new \DateTime('13:30'))->getTimestamp(),
+                    'expectedInternalValue' => '1970-01-01T13:30:00Z',
+                    'expectedValueAfterSave' => '1970-01-01T13:30:00+00:00',
                     'comment' => '',
                 ],
                 [
                     'inputValue' => '123',
                     'expectedValue' => '12:03',
-                    'expectedInternalValue' => '12:03',
-                    'expectedValueAfterSave' => (new \DateTime('12:03'))->getTimestamp(),
+                    'expectedInternalValue' => '1970-01-01T12:03:00Z',
+                    'expectedValueAfterSave' => '1970-01-01T12:03:00+00:00',
                     'comment' => '',
                 ],
                 [
                     'inputValue' => '12345',
                     'expectedValue' => '12:34',
-                    'expectedInternalValue' => '12:34',
-                    'expectedValueAfterSave' => (new \DateTime('12:34'))->getTimestamp(),
+                    'expectedInternalValue' => '1970-01-01T12:34:00Z',
+                    'expectedValueAfterSave' => '1970-01-01T12:34:00+00:00',
                     'comment' => '',
                 ],
                 [
                     'inputValue' => '12:04+5',
                     'expectedValue' => '12:09',
-                    'expectedInternalValue' => '12:09',
-                    'expectedValueAfterSave' => (new \DateTime('12:09'))->getTimestamp(),
+                    'expectedInternalValue' => '1970-01-01T12:09:00Z',
+                    'expectedValueAfterSave' => '1970-01-01T12:09:00+00:00',
                     'comment' => '',
                 ],
                 [
                     'inputValue' => '12:09-3',
                     'expectedValue' => '12:06',
-                    'expectedInternalValue' => '12:06',
-                    'expectedValueAfterSave' => (new \DateTime('12:06'))->getTimestamp(),
+                    'expectedInternalValue' => '1970-01-01T12:06:00Z',
+                    'expectedValueAfterSave' => '1970-01-01T12:06:00+00:00',
                     'comment' => '',
                 ],
             ],
index 518e1c9..03c115f 100644 (file)
@@ -166,20 +166,12 @@ class DataHandlerTest extends \TYPO3\TestingFramework\Core\Unit\UnitTestCase
     {
         // Three elements: input, timezone of input, expected output (UTC)
         return [
-            // German standard time (without DST) is one hour ahead of UTC
-            'date in 2016 in German timezone' => [
-                1457103519, 'Europe/Berlin', 1457103519 - 3600
+            'timestamp is passed through, as it is UTC' => [
+                1457103519, 'Europe/Berlin', 1457103519
             ],
-            'date in 1969 in German timezone' => [
-                -7200, 'Europe/Berlin', -10800
+            'ISO date is interpreted as local date and is output as correct timestamp' => [
+                '2017-06-07T00:10:00Z', 'Europe/Berlin', 1496787000
             ],
-            // Los Angeles is 8 hours behind UTC
-            'date in 2016 in Los Angeles timezone' => [
-                1457103519, 'America/Los_Angeles', 1457103519 + 28800
-            ],
-            'date in UTC' => [
-                1457103519, 'UTC', 1457103519
-            ]
         ];
     }
 
index 9f1c005..3278565 100644 (file)
@@ -731,7 +731,7 @@ class SchedulerModuleController extends \TYPO3\CMS\Backend\Module\BaseScriptClas
                 . $label
                 . '<div class="form-control-wrap">'
                     . '<div class="input-group" id="tceforms-datetimefield-task_start_row-wrapper">'
-                        . '<input name="tx_scheduler[start]_hr" value="' . $value . '" class="form-control t3js-datetimepicker t3js-clearable" data-date-type="datetime" data-date-offset="0" type="text" id="tceforms-datetimefield-task_start_row">'
+                        . '<input name="tx_scheduler[start]_hr" value="' . $value . '" class="form-control t3js-datetimepicker t3js-clearable" data-date-type="datetime" type="text" id="tceforms-datetimefield-task_start_row">'
                         . '<input name="tx_scheduler[start]" value="' . $taskInfo['start'] . '" type="hidden">'
                         . '<span class="input-group-btn"><label class="btn btn-default" for="tceforms-datetimefield-task_start_row"><span class="fa fa-calendar"></span></label></span>'
                     . '</div>'
@@ -747,7 +747,7 @@ class SchedulerModuleController extends \TYPO3\CMS\Backend\Module\BaseScriptClas
                 . BackendUtility::wrapInHelp($this->cshKey, 'task_end', $label)
                 . '<div class="form-control-wrap">'
                     . '<div class="input-group" id="tceforms-datetimefield-task_end_row-wrapper">'
-                        . '<input name="tx_scheduler[end]_hr" value="' . $value . '" class="form-control  t3js-datetimepicker t3js-clearable" data-date-type="datetime" data-date-offset="0" type="text" id="tceforms-datetimefield-task_end_row">'
+                        . '<input name="tx_scheduler[end]_hr" value="' . $value . '" class="form-control  t3js-datetimepicker t3js-clearable" data-date-type="datetime" type="text" id="tceforms-datetimefield-task_end_row">'
                         . '<input name="tx_scheduler[end]" value="' . $taskInfo['end'] . '" type="hidden">'
                         . '<span class="input-group-btn"><label class="btn btn-default" for="tceforms-datetimefield-task_end_row"><span class="fa fa-calendar"></span></label></span>'
                     . '</div>'