* CalDAV: fixed all-day events from Thunderbird with timezone were one day longer

using the timezone causes all sorts of problems, therefore ignoring it now and more robust end-time calculation
This commit is contained in:
ralf 2024-07-31 19:20:31 +02:00
parent acdf19e462
commit 4e975aa8f4
4 changed files with 45 additions and 37 deletions

View File

@ -377,7 +377,15 @@ class DateTime extends \DateTime
case 'integer': case 'integer':
case 'ts': case 'ts':
// EGroupware's integer timestamp is NOT the usual UTC timestamp, but has a timezone offset applied! // EGroupware's integer timestamp is NOT the usual UTC timestamp, but has a timezone offset applied!
return mktime(parent::format('H'),parent::format('i'),parent::format('s'),parent::format('m'),parent::format('d'),parent::format('Y')); // calendar_ical unfortunately sets different timezones, breaking all sorts of things, if we're not setting the TZ back to our server-timezone
if (date_default_timezone_get() !== self::$server_timezone->getName())
{
$tz_backup = date_default_timezone_get();
date_default_timezone_set(self::$server_timezone->getName());
}
$ret = mktime(parent::format('H'),parent::format('i'),parent::format('s'),parent::format('m'),parent::format('d'),parent::format('Y'));
if (isset($tz_backup)) date_default_timezone_set($tz_backup);
return $ret;
case 'utc': // alias for "U" / timestamp in UTC case 'utc': // alias for "U" / timestamp in UTC
return $this->getTimestamp(); return $this->getTimestamp();
case 'object': case 'object':

View File

@ -1559,15 +1559,8 @@ class calendar_boupdate extends calendar_bo
} }
if (!empty($event['end'])) if (!empty($event['end']))
{ {
$time = new Api\DateTime($event['end'], Api\DateTime::$user_timezone); $time = $this->so->startOfDay(new Api\DateTime($event['end'], Api\DateTime::$user_timezone));
$time->add('-1second');
// Check to see if switching timezones changes the date, we'll need to adjust for that
$end_event_timezone = clone $time;
$time->setServer();
$delta = (int)$end_event_timezone->format('z') - (int)$time->format('z');
$time->add("$delta days");
$time->setTime(23, 59, 59);
$event['end'] = Api\DateTime::to($time, 'ts'); $event['end'] = Api\DateTime::to($time, 'ts');
$save_event['end'] = $time; $save_event['end'] = $time;
} }
@ -1619,8 +1612,8 @@ class calendar_boupdate extends calendar_bo
{ {
if ($event['whole_day']) if ($event['whole_day'])
{ {
$time = $this->so->startOfDay(new Api\DateTime($date, Api\DateTime::$user_timezone)); // we use so->startOfDay(new Api\DateTime($time, Api\DateTime::$user_time)) as we not yet converted to server-time!
$date = Api\DateTime::to($time, 'ts'); $date = $this->so->startOfDay(new Api\DateTime($date, Api\DateTime::$user_timezone))->format('server');
} }
else else
{ {
@ -1642,7 +1635,7 @@ class calendar_boupdate extends calendar_bo
$alarm['time'] = $this->date2ts($alarm['time'], true); // user to server-time $alarm['time'] = $this->date2ts($alarm['time'], true); // user to server-time
} }
// remove alarms belonging to not longer existing or rejected participants // remove alarms belonging to no longer existing or rejected participants
if (!empty($alarm['owner']) && isset($expanded['participants'])) if (!empty($alarm['owner']) && isset($expanded['participants']))
{ {
// Don't auto-delete alarm if for all users // Don't auto-delete alarm if for all users

View File

@ -1465,7 +1465,10 @@ class calendar_ical extends calendar_boupdate
calendar_rrule::rrule2tz($event, $event_info['stored_event']['start'], calendar_rrule::rrule2tz($event, $event_info['stored_event']['start'],
$event_info['stored_event']['tzid']); $event_info['stored_event']['tzid']);
$event['tzid'] = $event_info['stored_event']['tzid']; if (empty($event['tzid']) && !empty($event_info['stored_event']['tzid']))
{
$event['tzid'] = $event_info['stored_event']['tzid'];
}
// avoid that iCal changes the organizer, which is not allowed // avoid that iCal changes the organizer, which is not allowed
$event['owner'] = $event_info['stored_event']['owner']; $event['owner'] = $event_info['stored_event']['owner'];
} }
@ -2625,13 +2628,13 @@ class calendar_ical extends calendar_boupdate
if (isset($attributes['params']['VALUE']) if (isset($attributes['params']['VALUE'])
&& $attributes['params']['VALUE'] == 'DATE') && $attributes['params']['VALUE'] == 'DATE')
{ {
$isDate = true; $event['whole_day'] = $isDate = true;
} }
$dtstart_ts = is_numeric($attributes['value']) ? $attributes['value'] : $this->date2ts($attributes['value']); $dtstart_ts = is_numeric($attributes['value']) ? $attributes['value'] : $this->date2ts($attributes['value']);
$vcardData['start'] = $dtstart_ts; $vcardData['start'] = $dtstart_ts;
// set event timezone from dtstart, if specified there // set event timezone from dtstart, if specified there
if (!empty($attributes['params']['TZID'])) if (!empty($attributes['params']['TZID']) && !$isDate)
{ {
// import TZID, if PHP understands it (we only care about TZID of starttime, // import TZID, if PHP understands it (we only care about TZID of starttime,
// as we store only a TZID for the whole event) // as we store only a TZID for the whole event)
@ -2681,7 +2684,7 @@ class calendar_ical extends calendar_boupdate
case 'DTEND': case 'DTEND':
$dtend_ts = is_numeric($attributes['value']) ? $attributes['value'] : $this->date2ts($attributes['value']); $dtend_ts = is_numeric($attributes['value']) ? $attributes['value'] : $this->date2ts($attributes['value']);
if (date('H:i:s',$dtend_ts) == '00:00:00') if (date('H:i:s',$dtend_ts) == '00:00:00' || isset($attributes['params']['VALUE']) && $attributes['params']['VALUE'] == 'DATE')
{ {
$dtend_ts -= 1; $dtend_ts -= 1;
} }

View File

@ -59,15 +59,15 @@ if (!defined('WEEK_s')) define('WEEK_s',7*DAY_s);
* Class to store all calendar data (storage object) * Class to store all calendar data (storage object)
* *
* Tables used by calendar_so: * Tables used by calendar_so:
* - egw_cal: general calendar data: cal_id, title, describtion, locations, range-start and -end dates * - egw_cal: general calendar data: cal_id, title, description, locations, range-start and -end dates
* - egw_cal_dates: start- and enddates (multiple entry per cal_id for recuring events!), recur_exception flag * - egw_cal_dates: start- and enddates (multiple entry per cal_id for recurring events!), recur_exception flag
* - egw_cal_user: participant info including status (multiple entries per cal_id AND startdate for recuring events) * - egw_cal_user: participant info including status (multiple entries per cal_id AND startdate for recuring events)
* - egw_cal_repeats: recur-data: type, interval, days etc. * - egw_cal_repeats: recur-data: type, interval, days etc.
* - egw_cal_extra: custom fields (multiple entries per cal_id possible) * - egw_cal_extra: custom fields (multiple entries per cal_id possible)
* *
* The new UI, BO and SO classes have a strict definition, in which timezone they operate: * The new UI, BO and SO classes have a strict definition, in which timezone they operate:
* UI only operates in user-time, so there have to be no conversation at all !!! * UI only operates in user-time, so there have to be no conversation at all !!!
* BO's functions take and return user-time only (!), they convert internaly everything to servertime, because * BO's functions take and return user-time only (!), they convert internally everything to servertime, because
* SO operates only on server-time * SO operates only on server-time
* *
* DB-model uses egw_cal_user.cal_status='X' for participants who got deleted. They never get returned by * DB-model uses egw_cal_user.cal_status='X' for participants who got deleted. They never get returned by
@ -79,13 +79,13 @@ if (!defined('WEEK_s')) define('WEEK_s',7*DAY_s);
* All update methods now take care to update modification time of (evtl. existing) series master too, * All update methods now take care to update modification time of (evtl. existing) series master too,
* to force an etag, ctag and sync-token change! Methods not doing that are private to this class. * to force an etag, ctag and sync-token change! Methods not doing that are private to this class.
* *
* range_start/_end in main-table contains start and end of whole event series (range_end is NULL for unlimited recuring events), * range_start/_end in main-table contains start and end of whole event series (range_end is NULL for unlimited recurring events),
* saving the need to always join dates table, to query non-enumerating recuring events (like CalDAV or ActiveSync does). * saving the need to always join dates table, to query non-enumerating recurring events (like CalDAV or ActiveSync does).
* This effectivly stores MIN(cal_start) and MAX(cal_end) permanently as column in main-table and improves speed tremendiously * This effectively stores MIN(cal_start) and MAX(cal_end) permanently as column in main-table and improves speed tremendously
* (few milisecs instead of more then 2 minutes on huge installations)! * (few millisecond instead of more than 2 minutes on huge installations)!
* It's set in calendar_so::save from start and end or recur_enddate, so nothing changes for higher level classes. * It's set in calendar_so::save from start and end or recur_enddate, so nothing changes for higher level classes.
* *
* egw_cal_user.cal_user_id contains since 14.3.001 only an md5-hash of a lowercased raw email address (not rfc822 address!). * egw_cal_user.cal_user_id contains since 14.3.001 only a md5-hash of a lowercased raw email address (not rfc822 address!).
* Real email address and other possible attendee information for iCal or CalDAV are stored in cal_user_attendee. * Real email address and other possible attendee information for iCal or CalDAV are stored in cal_user_attendee.
* This allows a short 32byte ascii cal_user_id and also storing attendee information for accounts and contacts. * This allows a short 32byte ascii cal_user_id and also storing attendee information for accounts and contacts.
* Outside of this class uid for email address is still "e$cn <$email>" or "e$email". * Outside of this class uid for email address is still "e$cn <$email>" or "e$email".
@ -93,6 +93,10 @@ if (!defined('WEEK_s')) define('WEEK_s',7*DAY_s);
* egw_cal_user.cal_user_id for DB and calendar_so::combine_user($user_type, $user_id, $user_attendee) to generate * egw_cal_user.cal_user_id for DB and calendar_so::combine_user($user_type, $user_id, $user_attendee) to generate
* uid used outside of this class. Both methods are unchanged when using with their default parameters. * uid used outside of this class. Both methods are unchanged when using with their default parameters.
* *
* Whole-day-events are stored with a start-time of 00:00:00 and an end-time of 23:59:59 in the server-timezone.
* They are logically understood as floating-date events, so in whatever timezone they occur the whole day!
* There is NO flag in the schema for whole-day events, they are detected by having the above start- and end-times.
*
* @ToDo drop egw_cal_repeats table in favor of a rrule colum in main table (saves always used left join and allows to store all sorts of rrules) * @ToDo drop egw_cal_repeats table in favor of a rrule colum in main table (saves always used left join and allows to store all sorts of rrules)
*/ */
class calendar_so class calendar_so
@ -480,7 +484,7 @@ class calendar_so
} }
} }
// check if we have a real recurance, if not set $recur_date=0 // check if we have a real recurrence, if not set $recur_date=0
if (is_array($ids) || empty($events[(int)$ids]['recur_type'])) if (is_array($ids) || empty($events[(int)$ids]['recur_type']))
{ {
$recur_date = 0; $recur_date = 0;
@ -3053,7 +3057,7 @@ ORDER BY cal_user_type, cal_usre_id
* Check if the event is the whole day * Check if the event is the whole day
* *
* @param array $event event (all timestamps in servertime) * @param array $event event (all timestamps in servertime)
* @return boolean true if whole day event within its timezone, false othwerwise * @return boolean true if whole day event within its timezone, false otherwise
*/ */
function isWholeDay($event) function isWholeDay($event)
{ {
@ -3072,27 +3076,27 @@ ORDER BY cal_user_type, cal_usre_id
$timezone = self::$tz_cache[$event['tzid']]; $timezone = self::$tz_cache[$event['tzid']];
} }
$start_time = new Api\DateTime($event['start'],Api\DateTime::$server_timezone); $start_time = new Api\DateTime($event['start'],Api\DateTime::$server_timezone);
$start_time->setTimezone($timezone);
$end_time = new Api\DateTime($event['end'],Api\DateTime::$server_timezone); $end_time = new Api\DateTime($event['end'],Api\DateTime::$server_timezone);
// by our schema-definition whole-day means 00:00-23:59 in server-timezone
if ($start_time->format('H:i') === '00:00' && $end_time->format('H:i') === '23:59')
{
return true;
}
// as current code used the event-timezone, lets check that too
$start_time->setTimezone($timezone);
$end_time->setTimezone($timezone); $end_time->setTimezone($timezone);
//error_log(__FILE__.'['.__LINE__.'] '.__METHOD__. return $start_time->format('H:i') === '00:00' && $end_time->format('H:i') === '23:59';
// '(): ' . $start . '-' . $end);
$start = Api\DateTime::to($start_time,'array');
$end = Api\DateTime::to($end_time,'array');
return !$start['hour'] && !$start['minute'] && $end['hour'] == 23 && $end['minute'] == 59;
} }
/** /**
* Moves a datetime to the beginning of the day within timezone * Moves a datetime to the beginning of the day within timezone
* *
* @param Api\DateTime $time the datetime entry * @param Api\DateTime $time the datetime entry
* @param string tz_id timezone * @param ?string $tz_id timezone
* *
* @return DateTime * @return DateTime
*/ */
function &startOfDay(Api\DateTime $time, $tz_id=null) function startOfDay(Api\DateTime $time, $tz_id=null)
{ {
if (empty($tz_id)) if (empty($tz_id))
{ {