Merge branch 'calendar-timestamp2datetime-refactoring'

This commit is contained in:
Thomas Bruederli 2012-07-11 10:00:52 +02:00
commit 000c0410e2
24 changed files with 1632 additions and 5409 deletions

View file

@ -16,3 +16,8 @@ are used. They are packaged in a slightly modified version with this plugin.
iCalendar parsing is done with the help of the Horde_iCalendar class. A copy iCalendar parsing is done with the help of the Horde_iCalendar class. A copy
of that class with all its dependencies is part of this package. In order of that class with all its dependencies is part of this package. In order
to update it, execute lib/get_horde_icalendar.sh > lib/Horde_iCalendar.php to update it, execute lib/get_horde_icalendar.sh > lib/Horde_iCalendar.php
IMPORTANT
---------
The calendar module makes heavy use of PHP's DateTime as well as DateInterval
classes. The latter one requires at least PHP 5.3.0 to run.

View file

@ -8,7 +8,7 @@
* @author Thomas Bruederli <bruederli@kolabsys.com> * @author Thomas Bruederli <bruederli@kolabsys.com>
* *
* Copyright (C) 2010, Lazlo Westerhof <hello@lazlo.me> * Copyright (C) 2010, Lazlo Westerhof <hello@lazlo.me>
* Copyright (C) 2011, Kolab Systems AG <contact@kolabsys.com> * Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@ -872,7 +872,7 @@ class calendar extends rcube_plugin
$events = $this->get_ical()->import_from_file($_FILES['_data']['tmp_name']); $events = $this->get_ical()->import_from_file($_FILES['_data']['tmp_name']);
$count = $errors = 0; $count = $errors = 0;
$rangestart = $_REQUEST['_range'] ? strtotime("now -" . intval($_REQUEST['_range']) . " months") : 0; $rangestart = $_REQUEST['_range'] ? date_create("now -" . intval($_REQUEST['_range']) . " months") : 0;
foreach ($events as $event) { foreach ($events as $event) {
// TODO: correctly handle recurring events which start before $rangestart // TODO: correctly handle recurring events which start before $rangestart
if ($event['end'] < $rangestart && (!$event['recurrence'] || ($event['recurrence']['until'] && $event['recurrence']['until'] < $rangestart))) if ($event['end'] < $rangestart && (!$event['recurrence'] || ($event['recurrence']['until'] && $event['recurrence']['until'] < $rangestart)))
@ -927,6 +927,8 @@ class calendar extends rcube_plugin
if ($calendars[$calid]) { if ($calendars[$calid]) {
$calname = $calendars[$calid]['name'] ? $calendars[$calid]['name'] : $calid; $calname = $calendars[$calid]['name'] ? $calendars[$calid]['name'] : $calid;
$calname = preg_replace('/[^a-z0-9_.-]/i', '', html_entity_decode($calname)); // to 7bit ascii
if (empty($calname)) $calname = $calid;
$events = $this->driver->load_events($start, $end, null, $calid, 0); $events = $this->driver->load_events($start, $end, null, $calid, 0);
} }
else else
@ -1107,12 +1109,17 @@ class calendar extends rcube_plugin
} }
/** /**
* Convert the given date string into a GMT-based time stamp * Shift dates into user's current timezone
*/ */
function fromGMT($datetime) private function adjust_timezone($dt)
{ {
$ts = is_numeric($datetime) ? $datetime : strtotime($datetime); if (is_numeric($dt))
return $ts + $this->gmt_offset; $dt = new DateTime('@'.$td);
else if (is_string($dt))
$dt = new DateTime($dt);
$dt->setTimezone($this->timezone);
return $dt;
} }
/** /**
@ -1139,16 +1146,19 @@ class calendar extends rcube_plugin
// compose a human readable strings for alarms_text and recurrence_text // compose a human readable strings for alarms_text and recurrence_text
if ($event['alarms']) if ($event['alarms'])
$event['alarms_text'] = $this->_alarms_text($event['alarms']); $event['alarms_text'] = $this->_alarms_text($event['alarms']);
if ($event['recurrence']) if ($event['recurrence']) {
$event['recurrence_text'] = $this->_recurrence_text($event['recurrence']); $event['recurrence_text'] = $this->_recurrence_text($event['recurrence']);
if ($event['recurrence']['UNTIL'])
$event['recurrence']['UNTIL'] = $this->adjust_timezone($event['recurrence']['UNTIL'])->format('c');
}
foreach ((array)$event['attachments'] as $k => $attachment) { foreach ((array)$event['attachments'] as $k => $attachment) {
$event['attachments'][$k]['classname'] = rcmail_filetype2classname($attachment['mimetype'], $attachment['name']); $event['attachments'][$k]['classname'] = rcmail_filetype2classname($attachment['mimetype'], $attachment['name']);
} }
return array( return array(
'start' => gmdate('c', $this->fromGMT($event['start'])), // client treats date strings as they were in users's timezone 'start' => $this->adjust_timezone($event['start'])->format('c'),
'end' => gmdate('c', $this->fromGMT($event['end'])), // so shift timestamps to users's timezone and render a date string 'end' => $this->adjust_timezone($event['end'])->format('c'),
'title' => strval($event['title']), 'title' => strval($event['title']),
'description' => strval($event['description']), 'description' => strval($event['description']),
'location' => strval($event['location']), 'location' => strval($event['location']),
@ -1167,8 +1177,8 @@ class calendar extends rcube_plugin
foreach ($alarms as $alarm) { foreach ($alarms as $alarm) {
$out[] = array( $out[] = array(
'id' => $alarm['id'], 'id' => $alarm['id'],
'start' => gmdate('c', $this->fromGMT($alarm['start'])), 'start' => $this->adjust_timezone($alarm['start'])->format('c'),
'end' => gmdate('c', $this->fromGMT($alarm['end'])), 'end' => $this->adjust_timezone($alarm['end'])->format('c'),
'allDay' => ($event['allday'] == 1)?true:false, 'allDay' => ($event['allday'] == 1)?true:false,
'title' => $alarm['title'], 'title' => $alarm['title'],
'location' => $alarm['location'], 'location' => $alarm['location'],
@ -1297,7 +1307,7 @@ class calendar extends rcube_plugin
} }
$offset = $notify[0] * $mult; $offset = $notify[0] * $mult;
$refdate = $mult > 0 ? $event['end'] : $event['start']; $refdate = $mult > 0 ? $event['end'] : $event['start'];
$notify_at = $refdate + $offset; $notify_at = $refdate->format('U') + $offset;
} }
else { // absolute timestamp else { // absolute timestamp
$notify_at = $notify[0]; $notify_at = $notify[0];
@ -1319,11 +1329,11 @@ class calendar extends rcube_plugin
$k = strtoupper($k); $k = strtoupper($k);
switch ($k) { switch ($k) {
case 'UNTIL': case 'UNTIL':
$val = gmdate('Ymd\THis', $val); $val = $val->format('Ymd\THis');
break; break;
case 'EXDATE': case 'EXDATE':
foreach ((array)$val as $i => $ex) foreach ((array)$val as $i => $ex)
$val[$i] = gmdate('Ymd\THis', $ex); $val[$i] = $ex->format('Ymd\THis');
$val = join(',', (array)$val); $val = join(',', (array)$val);
break; break;
} }
@ -1432,8 +1442,8 @@ class calendar extends rcube_plugin
$this->driver->new_event(array( $this->driver->new_event(array(
'uid' => $this->generate_uid(), 'uid' => $this->generate_uid(),
'start' => $start, 'start' => new DateTime('@'.$start),
'end' => $start + $duration, 'end' => new DateTime('@'.($start + $duration)),
'allday' => $allday, 'allday' => $allday,
'title' => rtrim($title), 'title' => rtrim($title),
'free_busy' => $fb == 2 ? 'outofoffice' : ($fb ? 'busy' : 'free'), 'free_busy' => $fb == 2 ? 'outofoffice' : ($fb ? 'busy' : 'free'),
@ -1683,6 +1693,13 @@ class calendar extends rcube_plugin
*/ */
private function prepare_event(&$event, $action) private function prepare_event(&$event, $action)
{ {
// convert dates into DateTime objects in user's current timezone
$event['start'] = new DateTime($event['start'], $this->timezone);
$event['end'] = new DateTime($event['end'], $this->timezone);
if ($event['recurrence']['UNTIL'])
$event['recurrence']['UNTIL'] = new DateTime($event['recurrence']['UNTIL'], $this->timezone);
$attachments = array(); $attachments = array();
$eventid = 'cal:'.$event['id']; $eventid = 'cal:'.$event['id'];
if (is_array($_SESSION['event_session']) && $_SESSION['event_session']['id'] == $eventid) { if (is_array($_SESSION['event_session']) && $_SESSION['event_session']['id'] == $eventid) {
@ -1785,7 +1802,7 @@ class calendar extends rcube_plugin
public function event_date_text($event, $tzinfo = false) public function event_date_text($event, $tzinfo = false)
{ {
$fromto = ''; $fromto = '';
$duration = $event['end'] - $event['start']; $duration = $event['start']->diff($event['end'])->format('s');
$this->date_format_defaults(); $this->date_format_defaults();
$date_format = self::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format'])); $date_format = self::to_php_date_format($this->rc->config->get('calendar_date_format', $this->defaults['calendar_date_format']));
@ -1796,7 +1813,7 @@ class calendar extends rcube_plugin
if (($todate = format_date($event['end'], $date_format)) != $fromto) if (($todate = format_date($event['end'], $date_format)) != $fromto)
$fromto .= ' - ' . $todate; $fromto .= ' - ' . $todate;
} }
else if ($duration < 86400 && gmdate('d', $event['start']) == gmdate('d', $event['end'])) { else if ($duration < 86400 && $event['start']->format('d') == $event['end']->format('d')) {
$fromto = format_date($event['start'], $date_format) . ' ' . format_date($event['start'], $time_format) . $fromto = format_date($event['start'], $date_format) . ' ' . format_date($event['start'], $time_format) .
' - ' . format_date($event['end'], $time_format); ' - ' . format_date($event['end'], $time_format);
} }

View file

@ -6,7 +6,7 @@
* @author Thomas Bruederli <bruederli@kolabsys.com> * @author Thomas Bruederli <bruederli@kolabsys.com>
* *
* Copyright (C) 2010, Lazlo Westerhof <hello@lazlo.me> * Copyright (C) 2010, Lazlo Westerhof <hello@lazlo.me>
* Copyright (C) 2011, Kolab Systems AG <contact@kolabsys.com> * Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as

View file

@ -186,6 +186,13 @@ function rcube_calendar_ui(settings)
return d; return d;
}; };
// turn the given date into an ISO 8601 date string understandable by PHPs strtotime()
var date2servertime = function(date)
{
return date.getFullYear()+'-'+(date.getMonth()+1)+'-'+date.getDate()
+ 'T'+date.getHours()+':'+date.getMinutes()+':'+date.getSeconds();
}
// convert the given Date object into a unix timestamp respecting browser's and user's timezone settings // convert the given Date object into a unix timestamp respecting browser's and user's timezone settings
var date2unixtime = function(date) var date2unixtime = function(date)
{ {
@ -545,7 +552,7 @@ function rcube_calendar_ui(settings)
recurrence = $('#edit-recurrence-frequency').val(event.recurrence ? event.recurrence.FREQ : '').change(); recurrence = $('#edit-recurrence-frequency').val(event.recurrence ? event.recurrence.FREQ : '').change();
interval = $('select.edit-recurrence-interval').val(event.recurrence ? event.recurrence.INTERVAL : 1); interval = $('select.edit-recurrence-interval').val(event.recurrence ? event.recurrence.INTERVAL : 1);
rrtimes = $('#edit-recurrence-repeat-times').val(event.recurrence ? event.recurrence.COUNT : 1); rrtimes = $('#edit-recurrence-repeat-times').val(event.recurrence ? event.recurrence.COUNT : 1);
rrenddate = $('#edit-recurrence-enddate').val(event.recurrence && event.recurrence.UNTIL ? $.fullCalendar.formatDate(new Date(event.recurrence.UNTIL*1000), settings['date_format']) : ''); rrenddate = $('#edit-recurrence-enddate').val(event.recurrence && event.recurrence.UNTIL ? $.fullCalendar.formatDate($.fullCalendar.parseISO8601(event.recurrence.UNTIL), settings['date_format']) : '');
$('input.edit-recurrence-until:checked').prop('checked', false); $('input.edit-recurrence-until:checked').prop('checked', false);
var weekdays = ['SU','MO','TU','WE','TH','FR','SA']; var weekdays = ['SU','MO','TU','WE','TH','FR','SA'];
@ -642,8 +649,8 @@ function rcube_calendar_ui(settings)
// post data to server // post data to server
var data = { var data = {
calendar: event.calendar, calendar: event.calendar,
start: date2unixtime(start), start: date2servertime(start),
end: date2unixtime(end), end: date2servertime(end),
allday: allday.checked?1:0, allday: allday.checked?1:0,
title: title.val(), title: title.val(),
description: description.val(), description: description.val(),
@ -702,7 +709,7 @@ function rcube_calendar_ui(settings)
if (until == 'count') if (until == 'count')
data.recurrence.COUNT = rrtimes.val(); data.recurrence.COUNT = rrtimes.val();
else if (until == 'until') else if (until == 'until')
data.recurrence.UNTIL = date2unixtime(parse_datetime(endtime.val(), rrenddate.val())); data.recurrence.UNTIL = date2servertime(parse_datetime(endtime.val(), rrenddate.val()));
if (freq == 'WEEKLY') { if (freq == 'WEEKLY') {
var byday = []; var byday = [];
@ -2233,9 +2240,9 @@ function rcube_calendar_ui(settings)
// initalize the fullCalendar plugin // initalize the fullCalendar plugin
var fc = $('#calendar').fullCalendar({ var fc = $('#calendar').fullCalendar({
header: { header: {
left: 'prev,next today', right: 'prev,next today',
center: 'title', center: 'title',
right: 'agendaDay,agendaWeek,month,table' left: 'agendaDay,agendaWeek,month,table'
}, },
aspectRatio: 1, aspectRatio: 1,
date: viewdate.getDate(), date: viewdate.getDate(),
@ -2375,8 +2382,8 @@ function rcube_calendar_ui(settings)
var data = { var data = {
id: event.id, id: event.id,
calendar: event.calendar, calendar: event.calendar,
start: date2unixtime(event.start), start: date2servertime(event.start),
end: date2unixtime(event.end), end: date2servertime(event.end),
allday: allDay?1:0 allday: allDay?1:0
}; };
update_event_confirm('move', event, data); update_event_confirm('move', event, data);
@ -2393,8 +2400,8 @@ function rcube_calendar_ui(settings)
var data = { var data = {
id: event.id, id: event.id,
calendar: event.calendar, calendar: event.calendar,
start: date2unixtime(event.start), start: date2servertime(event.start),
end: date2unixtime(event.end) end: date2servertime(event.end)
}; };
update_event_confirm('resize', event, data); update_event_confirm('resize', event, data);
}, },

View file

@ -8,7 +8,7 @@
* @author Thomas Bruederli <bruederli@kolabsys.com> * @author Thomas Bruederli <bruederli@kolabsys.com>
* *
* Copyright (C) 2010, Lazlo Westerhof <hello@lazlo.me> * Copyright (C) 2010, Lazlo Westerhof <hello@lazlo.me>
* Copyright (C) 2011, Kolab Systems AG <contact@kolabsys.com> * Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@ -32,20 +32,20 @@
* 'id' => 'Event ID used for editing', * 'id' => 'Event ID used for editing',
* 'uid' => 'Unique identifier of this event', * 'uid' => 'Unique identifier of this event',
* 'calendar' => 'Calendar identifier to add event to or where the event is stored', * 'calendar' => 'Calendar identifier to add event to or where the event is stored',
* 'start' => <unixtime>, // Event start date/time as unix timestamp * 'start' => DateTime, // Event start date/time as DateTime object
* 'end' => <unixtime>, // Event end date/time as unix timestamp * 'end' => DateTime, // Event end date/time as DateTime object
* 'allday' => true|false, // Boolean flag if this is an all-day event * 'allday' => true|false, // Boolean flag if this is an all-day event
* 'changed' => <unixtime>, // Last modification date of event * 'changed' => DateTime, // Last modification date of event
* 'title' => 'Event title/summary', * 'title' => 'Event title/summary',
* 'location' => 'Location string', * 'location' => 'Location string',
* 'description' => 'Event description', * 'description' => 'Event description',
* 'recurrence' => array( // Recurrence definition according to iCalendar (RFC 2445) specification as list of key-value pairs * 'recurrence' => array( // Recurrence definition according to iCalendar (RFC 2445) specification as list of key-value pairs
* 'FREQ' => 'DAILY|WEEKLY|MONTHLY|YEARLY', * 'FREQ' => 'DAILY|WEEKLY|MONTHLY|YEARLY',
* 'INTERVAL' => 1...n, * 'INTERVAL' => 1...n,
* 'UNTIL' => <unixtime>, * 'UNTIL' => DateTime,
* 'COUNT' => 1..n, // number of times * 'COUNT' => 1..n, // number of times
* // + more properties (see http://www.kanzaki.com/docs/ical/recur.html) * // + more properties (see http://www.kanzaki.com/docs/ical/recur.html)
* 'EXDATE' => array(), // list of <unixtime>s of exception Dates/Times * 'EXDATE' => array(), // list of DateTime objects of exception Dates/Times
* ), * ),
* 'recurrence_id' => 'ID of the recurrence group', // usually the ID of the starting event * 'recurrence_id' => 'ID of the recurrence group', // usually the ID of the starting event
* 'categories' => 'Event category', * 'categories' => 'Event category',
@ -157,8 +157,8 @@ abstract class calendar_driver
* *
* @param array Hash array with event properties: * @param array Hash array with event properties:
* id: Event identifier * id: Event identifier
* start: Event start date/time as unix timestamp * start: Event start date/time as DateTime object
* end: Event end date/time as unix timestamp * end: Event end date/time as DateTime object
* allday: Boolean flag if this is an all-day event * allday: Boolean flag if this is an all-day event
* @return boolean True on success, False on error * @return boolean True on success, False on error
*/ */
@ -169,8 +169,8 @@ abstract class calendar_driver
* *
* @param array Hash array with event properties: * @param array Hash array with event properties:
* id: Event identifier * id: Event identifier
* start: Event start date/time as unix timestamp in user timezone * start: Event start date/time as DateTime object with timezone
* end: Event end date/time as unix timestamp in user timezone * end: Event end date/time as DateTime object with timezone
* @return boolean True on success, False on error * @return boolean True on success, False on error
*/ */
abstract function resize_event($event); abstract function resize_event($event);
@ -230,8 +230,8 @@ abstract class calendar_driver
* @return array A list of alarms, each encoded as hash array: * @return array A list of alarms, each encoded as hash array:
* id: Event identifier * id: Event identifier
* uid: Unique identifier of this event * uid: Unique identifier of this event
* start: Event start date/time as unix timestamp * start: Event start date/time as DateTime object
* end: Event end date/time as unix timestamp * end: Event end date/time as DateTime object
* allday: Boolean flag if this is an all-day event * allday: Boolean flag if this is an all-day event
* title: Event title/summary * title: Event title/summary
* location: Location string * location: Location string

View file

@ -8,7 +8,7 @@
* @author Thomas Bruederli <bruederli@kolabsys.com> * @author Thomas Bruederli <bruederli@kolabsys.com>
* *
* Copyright (C) 2010, Lazlo Westerhof <hello@lazlo.me> * Copyright (C) 2010, Lazlo Westerhof <hello@lazlo.me>
* Copyright (C) 2011, Kolab Systems AG <contact@kolabsys.com> * Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@ -27,6 +27,8 @@
class database_driver extends calendar_driver class database_driver extends calendar_driver
{ {
const DB_DATE_FORMAT = 'Y-m-d H:i:s';
// features this backend supports // features this backend supports
public $alarms = true; public $alarms = true;
public $attendees = true; public $attendees = true;
@ -40,6 +42,7 @@ class database_driver extends calendar_driver
private $calendars = array(); private $calendars = array();
private $calendar_ids = ''; private $calendar_ids = '';
private $free_busy_map = array('free' => 0, 'busy' => 1, 'out-of-office' => 2, 'outofoffice' => 2, 'tentative' => 3); private $free_busy_map = array('free' => 0, 'busy' => 1, 'out-of-office' => 2, 'outofoffice' => 2, 'tentative' => 3);
private $server_timezone;
private $db_events = 'events'; private $db_events = 'events';
private $db_calendars = 'calendars'; private $db_calendars = 'calendars';
@ -56,6 +59,7 @@ class database_driver extends calendar_driver
{ {
$this->cal = $cal; $this->cal = $cal;
$this->rc = $cal->rc; $this->rc = $cal->rc;
$this->server_timezone = new DateTimeZone(date_default_timezone_get());
// load library classes // load library classes
require_once($this->cal->home . '/lib/Horde_Date_Recurrence.php'); require_once($this->cal->home . '/lib/Horde_Date_Recurrence.php');
@ -215,14 +219,14 @@ class database_driver extends calendar_driver
$query = $this->rc->db->query(sprintf( $query = $this->rc->db->query(sprintf(
"INSERT INTO " . $this->db_events . " "INSERT INTO " . $this->db_events . "
(calendar_id, created, changed, uid, start, end, all_day, recurrence, title, description, location, categories, free_busy, priority, sensitivity, attendees, alarms, notifyat) (calendar_id, created, changed, uid, start, end, all_day, recurrence, title, description, location, categories, free_busy, priority, sensitivity, attendees, alarms, notifyat)
VALUES (?, %s, %s, ?, %s, %s, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)", VALUES (?, %s, %s, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
$this->rc->db->now(), $this->rc->db->now(),
$this->rc->db->now(), $this->rc->db->now()
$this->rc->db->fromunixtime($event['start']),
$this->rc->db->fromunixtime($event['end'])
), ),
$event['calendar'], $event['calendar'],
strval($event['uid']), strval($event['uid']),
$event['start']->format(self::DB_DATE_FORMAT),
$event['end']->format(self::DB_DATE_FORMAT),
intval($event['all_day']), intval($event['all_day']),
$event['_recurrence'], $event['_recurrence'],
strval($event['title']), strval($event['title']),
@ -299,20 +303,23 @@ class database_driver extends calendar_driver
case 'future': case 'future':
if ($master['id'] != $event['id']) { if ($master['id'] != $event['id']) {
// set until-date on master event, then save this instance as new recurring event // set until-date on master event, then save this instance as new recurring event
$master['recurrence']['UNTIL'] = $event['start'] - 86400; $master['recurrence']['UNTIL'] = clone $event['start'];
$master['recurrence']['UNTIL']->sub(new DateInterval('P1D'));
unset($master['recurrence']['COUNT']); unset($master['recurrence']['COUNT']);
$update_master = true; $update_master = true;
// if recurrence COUNT, update value to the correct number of future occurences // if recurrence COUNT, update value to the correct number of future occurences
if ($event['recurrence']['COUNT']) { if ($event['recurrence']['COUNT']) {
$fromdate = clone $master['start'];
$fromdate->setTimezone($this->server_timezone);
$sqlresult = $this->rc->db->query(sprintf( $sqlresult = $this->rc->db->query(sprintf(
"SELECT event_id FROM " . $this->db_events . " "SELECT event_id FROM " . $this->db_events . "
WHERE calendar_id IN (%s) WHERE calendar_id IN (%s)
AND start >= %s AND start >= ?
AND recurrence_id=?", AND recurrence_id=?",
$this->calendar_ids, $this->calendar_ids
$this->rc->db->fromunixtime($event['start'])
), ),
$fromdate->format(self::DB_DATE_FORMAT),
$master['id']); $master['id']);
if ($count = $this->rc->db->num_rows($sqlresult)) if ($count = $this->rc->db->num_rows($sqlresult))
$event['recurrence']['COUNT'] = $count; $event['recurrence']['COUNT'] = $count;
@ -329,20 +336,21 @@ class database_driver extends calendar_driver
$event['recurrence_id'] = 0; $event['recurrence_id'] = 0;
// use start date from master but try to be smart on time or duration changes // use start date from master but try to be smart on time or duration changes
$old_start_date = date('Y-m-d', $old['start']); $old_start_date = $old['start']->format('Y-m-d');
$old_start_time = date('H:i', $old['start']); $old_start_time = $old['start']->format('H:i');
$old_duration = $old['end'] - $old['start']; $old_duration = $old['end']->format('U') - $old['start']->format('U');
$new_start_date = date('Y-m-d', $event['start']); $new_start_date = $event['start']->format('Y-m-d');
$new_start_time = date('H:i', $event['start']); $new_start_time = $event['start']->format('H:i');
$new_duration = $event['end'] - $event['start']; $new_duration = $event['end']->format('U') - $event['start']->format('U');
$diff = $old_start_date != $new_start_date || $old_start_time != $new_start_time || $old_duration != $new_duration; $diff = $old_start_date != $new_start_date || $old_start_time != $new_start_time || $old_duration != $new_duration;
// shifted or resized // shifted or resized
if ($diff && ($old_start_date == $new_start_date || $old_duration == $new_duration)) { if ($diff && ($old_start_date == $new_start_date || $old_duration == $new_duration)) {
$event['start'] = $master['start'] + ($event['start'] - $old['start']); $event['start'] = $master['start']->add($old['start']->diff($event['start']));
$event['end'] = $event['start'] + $new_duration; $event['end'] = clone $event['start'];
$event['end']->add(new DateInterval('PT'.$new_duration.'S'));
} }
break; break;
} }
@ -363,6 +371,12 @@ class database_driver extends calendar_driver
*/ */
private function _save_preprocess($event) private function _save_preprocess($event)
{ {
// shift dates to server's timezone
$event['start'] = clone $event['start'];
$event['start']->setTimezone($this->server_timezone);
$event['end'] = clone $event['end'];
$event['end']->setTimezone($this->server_timezone);
// compose vcalendar-style recurrencue rule from structured data // compose vcalendar-style recurrencue rule from structured data
$rrule = $event['recurrence'] ? calendar::to_rrule($event['recurrence']) : ''; $rrule = $event['recurrence'] ? calendar::to_rrule($event['recurrence']) : '';
$event['_recurrence'] = rtrim($rrule, ';'); $event['_recurrence'] = rtrim($rrule, ';');
@ -396,7 +410,7 @@ class database_driver extends calendar_driver
*/ */
private function _get_notification($event) private function _get_notification($event)
{ {
if ($event['alarms'] && $event['start'] > time()) { if ($event['alarms'] && $event['start'] > new DateTime()) {
$alarm = calendar::get_next_alarm($event); $alarm = calendar::get_next_alarm($event);
if ($alarm['time'] && $alarm['action'] == 'DISPLAY') if ($alarm['time'] && $alarm['action'] == 'DISPLAY')
@ -416,9 +430,11 @@ class database_driver extends calendar_driver
{ {
$event = $this->_save_preprocess($event); $event = $this->_save_preprocess($event);
$sql_set = array(); $sql_set = array();
$set_cols = array('all_day', 'recurrence_id', 'title', 'description', 'location', 'categories', 'free_busy', 'priority', 'sensitivity', 'attendees', 'alarms', 'notifyat'); $set_cols = array('start', 'end', 'all_day', 'recurrence_id', 'title', 'description', 'location', 'categories', 'free_busy', 'priority', 'sensitivity', 'attendees', 'alarms', 'notifyat');
foreach ($set_cols as $col) { foreach ($set_cols as $col) {
if (isset($event[$col])) if (is_object($event[$col]) && is_a($event[$col], 'DateTime'))
$sql_set[] = $this->rc->db->quote_identifier($col) . '=' . $this->rc->db->quote($event[$col]->format(self::DB_DATE_FORMAT));
else if (isset($event[$col]))
$sql_set[] = $this->rc->db->quote_identifier($col) . '=' . $this->rc->db->quote($event[$col]); $sql_set[] = $this->rc->db->quote_identifier($col) . '=' . $this->rc->db->quote($event[$col]);
} }
@ -430,12 +446,10 @@ class database_driver extends calendar_driver
$query = $this->rc->db->query(sprintf( $query = $this->rc->db->query(sprintf(
"UPDATE " . $this->db_events . " "UPDATE " . $this->db_events . "
SET changed=%s, start=%s, end=%s %s SET changed=%s %s
WHERE event_id=? WHERE event_id=?
AND calendar_id IN (" . $this->calendar_ids . ")", AND calendar_id IN (" . $this->calendar_ids . ")",
$this->rc->db->now(), $this->rc->db->now(),
$this->rc->db->fromunixtime($event['start']),
$this->rc->db->fromunixtime($event['end']),
($sql_set ? ', ' . join(', ', $sql_set) : '') ($sql_set ? ', ' . join(', ', $sql_set) : '')
), ),
$event['id'] $event['id']
@ -490,20 +504,23 @@ class database_driver extends calendar_driver
$recurrence = new calendar_recurrence($this->cal, $event); $recurrence = new calendar_recurrence($this->cal, $event);
$duration = $event['end'] - $event['start']; $duration = $event['start']->diff($event['end']);
while ($next_ts = $recurrence->next_start()) { while ($next_start = $recurrence->next_start()) {
$notify_at = $this->_get_notification(array('alarms' => $event['alarms'], 'start' => $next_ts, 'end' => $next_ts + $duration)); $next_start->setTimezone($this->server_timezone);
$next_end = clone $next_start;
$next_end->add($duration);
$notify_at = $this->_get_notification(array('alarms' => $event['alarms'], 'start' => $next_start, 'end' => $next_end));
$query = $this->rc->db->query(sprintf( $query = $this->rc->db->query(sprintf(
"INSERT INTO " . $this->db_events . " "INSERT INTO " . $this->db_events . "
(calendar_id, recurrence_id, created, changed, uid, start, end, all_day, recurrence, title, description, location, categories, free_busy, priority, sensitivity, alarms, notifyat) (calendar_id, recurrence_id, created, changed, uid, start, end, all_day, recurrence, title, description, location, categories, free_busy, priority, sensitivity, alarms, notifyat)
SELECT calendar_id, ?, %s, %s, uid, %s, %s, all_day, recurrence, title, description, location, categories, free_busy, priority, sensitivity, alarms, ? SELECT calendar_id, ?, %s, %s, uid, ?, ?, all_day, recurrence, title, description, location, categories, free_busy, priority, sensitivity, alarms, ?
FROM " . $this->db_events . " WHERE event_id=? AND calendar_id IN (" . $this->calendar_ids . ")", FROM " . $this->db_events . " WHERE event_id=? AND calendar_id IN (" . $this->calendar_ids . ")",
$this->rc->db->now(), $this->rc->db->now(),
$this->rc->db->now(), $this->rc->db->now()
$this->rc->db->fromunixtime($next_ts),
$this->rc->db->fromunixtime($next_ts + $duration)
), ),
$event['id'], $event['id'],
$next_start->format(self::DB_DATE_FORMAT),
$next_end->format(self::DB_DATE_FORMAT),
$notify_at, $notify_at,
$event['id'] $event['id']
); );
@ -582,16 +599,20 @@ class database_driver extends calendar_driver
case 'future': case 'future':
if ($master['id'] != $event['id']) { if ($master['id'] != $event['id']) {
// set until-date on master event // set until-date on master event
$master['recurrence']['UNTIL'] = $event['start'] - 86400; $master['recurrence']['UNTIL'] = clone $event['start'];
$master['recurrence']['UNTIL']->sub(new DateInterval('P1D'));
unset($master['recurrence']['COUNT']); unset($master['recurrence']['COUNT']);
$update_master = true; $update_master = true;
// delete this and all future instances // delete this and all future instances
$fromdate = clone $old['start'];
$fromdate->setTimezone($this->server_timezone);
$query = $this->rc->db->query( $query = $this->rc->db->query(
"DELETE FROM " . $this->db_events . " "DELETE FROM " . $this->db_events . "
WHERE calendar_id IN (" . $this->calendar_ids . ") WHERE calendar_id IN (" . $this->calendar_ids . ")
AND start >= " . $this->rc->db->fromunixtime($old['start']) . " AND start >= ?
AND recurrence_id=?", AND recurrence_id=?",
$fromdate->format(self::DB_DATE_FORMAT),
$master['id'] $master['id']
); );
break; break;
@ -704,8 +725,8 @@ class database_driver extends calendar_driver
$free_busy_map = array_flip($this->free_busy_map); $free_busy_map = array_flip($this->free_busy_map);
$event['id'] = $event['event_id']; $event['id'] = $event['event_id'];
$event['start'] = strtotime($event['start']); $event['start'] = new DateTime($event['start']);
$event['end'] = strtotime($event['end']); $event['end'] = new DateTime($event['end']);
$event['allday'] = intval($event['all_day']); $event['allday'] = intval($event['all_day']);
$event['changed'] = strtotime($event['changed']); $event['changed'] = strtotime($event['changed']);
$event['free_busy'] = $free_busy_map[$event['free_busy']]; $event['free_busy'] = $free_busy_map[$event['free_busy']];
@ -719,9 +740,9 @@ class database_driver extends calendar_driver
if (is_numeric($rr[2])) if (is_numeric($rr[2]))
$rr[2] = intval($rr[2]); $rr[2] = intval($rr[2]);
else if ($rr[1] == 'UNTIL') else if ($rr[1] == 'UNTIL')
$rr[2] = strtotime($rr[2]); $rr[2] = date_create($rr[2]);
else if ($rr[1] == 'EXDATE') else if ($rr[1] == 'EXDATE')
$rr[2] = array_map('strtotime', explode(',', $rr[2])); $rr[2] = array_map('date_create', explode(',', $rr[2]));
$event['recurrence'][$rr[1]] = $rr[2]; $event['recurrence'][$rr[1]] = $rr[2];
} }
} }
@ -734,7 +755,7 @@ class database_driver extends calendar_driver
$attendees = array(); $attendees = array();
foreach (explode("\n", $event['attendees']) as $line) { foreach (explode("\n", $event['attendees']) as $line) {
$att = array(); $att = array();
foreach (rcube_explode_quoted_string(';', $line) as $prop) { foreach (rcube_utils::explode_quoted_string(';', $line) as $prop) {
list($key, $value) = explode("=", $prop); list($key, $value) = explode("=", $prop);
$att[strtolower($key)] = stripslashes(trim($value, '""')); $att[strtolower($key)] = stripslashes(trim($value, '""'));
} }
@ -793,7 +814,7 @@ class database_driver extends calendar_driver
public function dismiss_alarm($event_id, $snooze = 0) public function dismiss_alarm($event_id, $snooze = 0)
{ {
// set new notifyat time or unset if not snoozed // set new notifyat time or unset if not snoozed
$notify_at = $snooze > 0 ? date('Y-m-d H:i:s', time() + $snooze) : null; $notify_at = $snooze > 0 ? date(self::DB_DATE_FORMAT, time() + $snooze) : null;
$query = $this->rc->db->query(sprintf( $query = $this->rc->db->query(sprintf(
"UPDATE " . $this->db_events . " "UPDATE " . $this->db_events . "

View file

@ -182,7 +182,9 @@ class kolab_calendar
$this->events[$master_id] = $this->_to_rcube_event($record); $this->events[$master_id] = $this->_to_rcube_event($record);
if (($master = $this->events[$master_id]) && $master['recurrence']) { if (($master = $this->events[$master_id]) && $master['recurrence']) {
$this->_get_recurring_events($master, $master['start'], $master['start'] + 86400 * 365 * 10, $id); $limit = clone $master['start'];
$limit->add(new DateInterval('P10Y'));
$this->_get_recurring_events($record, $master['start'], $limit, $id);
} }
} }
@ -200,6 +202,10 @@ class kolab_calendar
*/ */
public function list_events($start, $end, $search = null, $virtual = 1, $query = array()) public function list_events($start, $end, $search = null, $virtual = 1, $query = array())
{ {
// convert to DateTime for comparisons
$start = new DateTime('@'.$start);
$end = new DateTime('@'.$end);
// query Kolab storage // query Kolab storage
$query[] = array('dtstart', '<=', $end); $query[] = array('dtstart', '<=', $end);
$query[] = array('dtend', '>=', $start); $query[] = array('dtend', '>=', $start);
@ -211,13 +217,11 @@ class kolab_calendar
} }
} }
$events = array();
foreach ((array)$this->storage->select($query) as $record) { foreach ((array)$this->storage->select($query) as $record) {
$event = $this->_to_rcube_event($record); $event = $this->_to_rcube_event($record);
$this->events[$event['id']] = $event; $this->events[$event['id']] = $event;
}
$events = array();
foreach ($this->events as $id => $event) {
// remember seen categories // remember seen categories
if ($event['categories']) if ($event['categories'])
$this->categories[$event['categories']]++; $this->categories[$event['categories']]++;
@ -249,9 +253,8 @@ class kolab_calendar
} }
// resolve recurring events // resolve recurring events
if ($event['recurrence'] && $virtual == 1) { if ($record['recurrence'] && $virtual == 1) {
unset($event['_attendees']); $events = array_merge($events, $this->_get_recurring_events($record, $start, $end));
$events = array_merge($events, $this->_get_recurring_events($event, $start, $end));
} }
} }
@ -375,21 +378,20 @@ class kolab_calendar
{ {
$recurrence = new kolab_date_recurrence($event); $recurrence = new kolab_date_recurrence($event);
$events = array();
$duration = $event['end'] - $event['start'];
$i = 0; $i = 0;
while ($rec_start = $recurrence->next_start(true)) { $events = array();
$rec_end = $rec_start + $duration; while ($next_event = $recurrence->next_instance()) {
$rec_id = $event['id'] . '-' . ++$i; $rec_start = $next_event['start']->format('U');
$rec_end = $next_event['end']->format('U');
$rec_id = $event['uid'] . '-' . ++$i;
// add to output if in range // add to output if in range
if (($rec_start <= $end && $rec_end >= $start) || ($event_id && $rec_id == $event_id)) { if (($next_event['start'] <= $end && $next_event['end'] >= $start) || ($event_id && $rec_id == $event_id)) {
$rec_event = $event; $rec_event = $this->_to_rcube_event($next_event);
$rec_event['id'] = $rec_id; $rec_event['id'] = $rec_id;
$rec_event['recurrence_id'] = $event['id']; $rec_event['recurrence_id'] = $event['uid'];
$rec_event['start'] = $rec_start;
$rec_event['end'] = $rec_end;
$rec_event['_instance'] = $i; $rec_event['_instance'] = $i;
unset($rec_event['_attendees']);
$events[] = $rec_event; $events[] = $rec_event;
if ($rec_id == $event_id) { if ($rec_id == $event_id) {
@ -397,7 +399,7 @@ class kolab_calendar
break; break;
} }
} }
else if ($rec_start > $end) // stop loop if out of range else if ($next_event['start'] > $end) // stop loop if out of range
break; break;
} }
@ -411,16 +413,18 @@ class kolab_calendar
{ {
$record['id'] = $record['uid']; $record['id'] = $record['uid'];
$record['calendar'] = $this->id; $record['calendar'] = $this->id;
/*
// convert from DateTime to unix timestamp // convert from DateTime to unix timestamp
if (is_a($record['start'], 'DateTime')) if (is_a($record['start'], 'DateTime'))
$record['start'] = $record['start']->format('U'); $record['start'] = $record['start']->format('U');
if (is_a($record['end'], 'DateTime')) if (is_a($record['end'], 'DateTime'))
$record['end'] = $record['end']->format('U'); $record['end'] = $record['end']->format('U');
*/
// all-day events go from 12:00 - 13:00 // all-day events go from 12:00 - 13:00
if ($record['end'] <= $record['start'] && $record['allday']) if ($record['end'] <= $record['start'] && $record['allday']) {
$record['end'] = $record['start'] + 3600; $record['end'] = clone $record['start'];
$record['end']->add(new DateTimeInterval('PT1H'));
}
if (!empty($record['_attachments'])) { if (!empty($record['_attachments'])) {
foreach ($record['_attachments'] as $key => $attachment) { foreach ($record['_attachments'] as $key => $attachment) {

View file

@ -380,7 +380,9 @@ class kolab_driver extends calendar_driver
// removing the first instance => just move to next occurence // removing the first instance => just move to next occurence
if ($master['id'] == $event['id']) { if ($master['id'] == $event['id']) {
$recurring = reset($storage->_get_recurring_events($event, $event['start'], $event['end'] + 86400 * 370, $event['id'].'-1')); $limit = clone $event['end'];
$limit->add(new DateInterval('P370D'));
$recurring = reset($storage->_get_recurring_events($event, $event['start'], $limit, $event['id'].'-1'));
$master['start'] = $recurring['start']; $master['start'] = $recurring['start'];
$master['end'] = $recurring['end']; $master['end'] = $recurring['end'];
if ($master['recurrence']['COUNT']) if ($master['recurrence']['COUNT'])
@ -397,7 +399,8 @@ class kolab_driver extends calendar_driver
$_SESSION['calendar_restore_event_data'] = $master; $_SESSION['calendar_restore_event_data'] = $master;
// set until-date on master event // set until-date on master event
$master['recurrence']['UNTIL'] = $event['start'] - 86400; $master['recurrence']['UNTIL'] = clone $event['start'];
$master['recurrence']['UNTIL']->sub(new DateInterval('P1D'));
unset($master['recurrence']['COUNT']); unset($master['recurrence']['COUNT']);
$success = $storage->update_event($master); $success = $storage->update_event($master);
break; break;
@ -539,7 +542,8 @@ class kolab_driver extends calendar_driver
case 'future': case 'future':
if ($master['id'] != $event['id']) { if ($master['id'] != $event['id']) {
// set until-date on master event // set until-date on master event
$master['recurrence']['UNTIL'] = $old['start'] - 86400; $master['recurrence']['UNTIL'] = clone $old['start'];
$master['recurrence']['UNTIL']->sub(new DateInterval('P1D'));
unset($master['recurrence']['COUNT']); unset($master['recurrence']['COUNT']);
$storage->update_event($master); $storage->update_event($master);
@ -555,7 +559,7 @@ class kolab_driver extends calendar_driver
// remove fixed weekday, will be re-set to the new weekday in kolab_calendar::insert_event() // remove fixed weekday, will be re-set to the new weekday in kolab_calendar::insert_event()
if (strlen($event['recurrence']['BYDAY']) == 2) if (strlen($event['recurrence']['BYDAY']) == 2)
unset($event['recurrence']['BYDAY']); unset($event['recurrence']['BYDAY']);
if ($master['recurrence']['BYMONTH'] == gmdate('n', $master['start'])) if ($master['recurrence']['BYMONTH'] == $master['start']->format('n'))
unset($event['recurrence']['BYMONTH']); unset($event['recurrence']['BYMONTH']);
$success = $storage->insert_event($event); $success = $storage->insert_event($event);
@ -567,26 +571,27 @@ class kolab_driver extends calendar_driver
$event['uid'] = $master['uid']; $event['uid'] = $master['uid'];
// use start date from master but try to be smart on time or duration changes // use start date from master but try to be smart on time or duration changes
$old_start_date = date('Y-m-d', $old['start']); $old_start_date = $old['start']->format('Y-m-d');
$old_start_time = date('H:i', $old['start']); $old_start_time = $old['start']->format('H:i');
$old_duration = $old['end'] - $old['start']; $old_duration = $old['end']->format('U') - $old['start']->format('U');
$new_start_date = date('Y-m-d', $event['start']); $new_start_date = $event['start']->format('Y-m-d');
$new_start_time = date('H:i', $event['start']); $new_start_time = $event['start']->format('H:i');
$new_duration = $event['end'] - $event['start']; $new_duration = $event['end']->format('U') - $event['start']->format('U');
$diff = $old_start_date != $new_start_date || $old_start_time != $new_start_time || $old_duration != $new_duration; $diff = $old_start_date != $new_start_date || $old_start_time != $new_start_time || $old_duration != $new_duration;
// shifted or resized // shifted or resized
if ($diff && ($old_start_date == $new_start_date || $old_duration == $new_duration)) { if ($diff && ($old_start_date == $new_start_date || $old_duration == $new_duration)) {
$event['start'] = $master['start'] + ($event['start'] - $old['start']); $event['start'] = $master['start']->add($old['start']->diff($event['start']));
$event['end'] = $event['start'] + $new_duration; $event['end'] = clone $event['start'];
$event['end']->add(new DateInterval('PT'.$new_duration.'S'));
// remove fixed weekday, will be re-set to the new weekday in kolab_calendar::update_event() // remove fixed weekday, will be re-set to the new weekday in kolab_calendar::update_event()
if ($old_start_date != $new_start_date) { if ($old_start_date != $new_start_date) {
if (strlen($event['recurrence']['BYDAY']) == 2) if (strlen($event['recurrence']['BYDAY']) == 2)
unset($event['recurrence']['BYDAY']); unset($event['recurrence']['BYDAY']);
if ($old['recurrence']['BYMONTH'] == gmdate('n', $old['start'])) if ($old['recurrence']['BYMONTH'] == $old['start']->format('n'))
unset($event['recurrence']['BYMONTH']); unset($event['recurrence']['BYMONTH']);
} }
} }

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -144,22 +144,23 @@ class calendar_ical
'uid' => $ve->getAttributeDefault('UID'), 'uid' => $ve->getAttributeDefault('UID'),
'changed' => $ve->getAttributeDefault('DTSTAMP', 0), 'changed' => $ve->getAttributeDefault('DTSTAMP', 0),
'title' => $ve->getAttributeDefault('SUMMARY'), 'title' => $ve->getAttributeDefault('SUMMARY'),
'start' => $ve->getAttribute('DTSTART'), 'start' => $this->_date2time($ve->getAttribute('DTSTART')),
'end' => $ve->getAttribute('DTEND'), 'end' => $this->_date2time($ve->getAttribute('DTEND')),
// set defaults // set defaults
'free_busy' => 'busy', 'free_busy' => 'busy',
'priority' => 0, 'priority' => 0,
); );
// check for all-day dates // check for all-day dates
if (is_array($event['start'])) { if (is_array($ve->getAttribute('DTSTART')))
// create timestamp at 12:00 in user's timezone
$event['start'] = $this->_date2time($event['start']);
$event['allday'] = true; $event['allday'] = true;
}
if (is_array($event['end'])) { if ($event['allday'])
$event['end'] = $this->_date2time($event['end']) - 23 * 3600; $event['end']->sub(new DateInterval('PT23H'));
}
// assign current timezone to event start/end
$event['start']->setTimezone($this->cal->timezone);
$event['end']->setTimezone($this->cal->timezone);
// map other attributes to internal fields // map other attributes to internal fields
$_attendees = array(); $_attendees = array();
@ -215,7 +216,7 @@ class calendar_ical
$params[$k] = $v; $params[$k] = $v;
} }
if ($params['UNTIL']) if ($params['UNTIL'])
$params['UNTIL'] = $ve->_parseDateTime($params['UNTIL']); $params['UNTIL'] = date_create($params['UNTIL']);
if (!$params['INTERVAL']) if (!$params['INTERVAL'])
$params['INTERVAL'] = 1; $params['INTERVAL'] = 1;
@ -309,11 +310,11 @@ class calendar_ical
private function _date2time($prop) private function _date2time($prop)
{ {
// create timestamp at 12:00 in user's timezone // create timestamp at 12:00 in user's timezone
if (is_array($prop)) { if (is_array($prop))
$date = new DateTime(sprintf('%04d%02d%02dT120000', $prop['year'], $prop['month'], $prop['mday']), $this->cal->timezone); return date_create(sprintf('%04d%02d%02dT120000', $prop['year'], $prop['month'], $prop['mday']), $this->cal->timezone);
console($prop, $date->format('r')); else if (is_numeric($prop))
return $date->getTimestamp(); return date_create('@'.$prop);
}
return $prop; return $prop;
} }
@ -353,15 +354,17 @@ class calendar_ical
foreach ($events as $event) { foreach ($events as $event) {
$vevent = "BEGIN:VEVENT" . self::EOL; $vevent = "BEGIN:VEVENT" . self::EOL;
$vevent .= "UID:" . self::escpape($event['uid']) . self::EOL; $vevent .= "UID:" . self::escpape($event['uid']) . self::EOL;
$vevent .= "DTSTAMP:" . gmdate('Ymd\THis\Z', $event['changed'] ? $event['changed'] : time()) . self::EOL; $vevent .= $this->format_datetime("DTSTAMP", $event['changed'] ?: new DateTime(), false, true) . self::EOL;
// correctly set all-day dates // correctly set all-day dates
if ($event['allday']) { if ($event['allday']) {
$vevent .= "DTSTART;VALUE=DATE:" . gmdate('Ymd', $event['start'] + $this->cal->gmt_offset) . self::EOL; $event['end'] = clone $event['end'];
$vevent .= "DTEND;VALUE=DATE:" . gmdate('Ymd', $event['end'] + $this->cal->gmt_offset + 86400) . self::EOL; // ends the next day $event['end']->add(new DateInterval('P1D')); // ends the next day
$vevent .= $this->format_datetime("DTSTART", $event['start'], true) . self::EOL;
$vevent .= $this->format_datetime("DTEND", $event['end'], true) . self::EOL;
} }
else { else {
$vevent .= "DTSTART:" . gmdate('Ymd\THis\Z', $event['start']) . self::EOL; $vevent .= $this->format_datetime("DTSTART", $event['start'], false) . self::EOL;
$vevent .= "DTEND:" . gmdate('Ymd\THis\Z', $event['end']) . self::EOL; $vevent .= $this->format_datetime("DTEND", $event['end'], false) . self::EOL;
} }
$vevent .= "SUMMARY:" . self::escpape($event['title']) . self::EOL; $vevent .= "SUMMARY:" . self::escpape($event['title']) . self::EOL;
$vevent .= "DESCRIPTION:" . self::escpape($event['description']) . self::EOL; $vevent .= "DESCRIPTION:" . self::escpape($event['description']) . self::EOL;
@ -425,6 +428,25 @@ class calendar_ical
return rcube_vcard::rfc2425_fold($ical); return rcube_vcard::rfc2425_fold($ical);
} }
private function format_datetime($attr, $dt, $dateonly = false, $utc = false)
{
if (is_numeric($dt))
$dt = new DateTime('@'.$dt);
if ($utc)
$dt->setTimezone(new DateTimeZone('UTC'));
if ($dateonly) {
return $attr . ';VALUE=DATE:' . $dt->format('Ymd');
}
else {
// <ATTR>;TZID=Europe/Zurich:20120706T210000
$tz = $dt->getTimezone();
$tzid = $tz && $tz->getName() != 'UTC' ? ';TZID=' . $tz->getName() : '';
return $attr . $tzid . ':' . $dt->format('Ymd\THis' . ($tzid ? '' : 'Z'));
}
}
private function escpape($str) private function escpape($str)
{ {
return preg_replace('/(?<!\\\\)([\:\;\,\\n\\r])/', '\\\$1', $str); return preg_replace('/(?<!\\\\)([\:\;\,\\n\\r])/', '\\\$1', $str);

View file

@ -9,7 +9,7 @@
* @author Thomas Bruederli <bruederli@kolabsys.com> * @author Thomas Bruederli <bruederli@kolabsys.com>
* @package @package_name@ * @package @package_name@
* *
* Copyright (C) 2011, Kolab Systems AG <contact@kolabsys.com> * Copyright (C) 2012, Kolab Systems AG <contact@kolabsys.com>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
@ -28,9 +28,9 @@ class calendar_recurrence
{ {
private $cal; private $cal;
private $event; private $event;
private $next;
private $engine; private $engine;
private $tz_offset = 0; private $duration;
private $dst_start = false;
private $hour = 0; private $hour = 0;
/** /**
@ -42,30 +42,30 @@ class calendar_recurrence
function __construct($cal, $event) function __construct($cal, $event)
{ {
$this->cal = $cal; $this->cal = $cal;
$this->event = $event;
$this->next = new Horde_Date($event['start'], $cal->timezone->getName());
$this->hour = $this->next->hour;
if (is_object($event['start']) && is_object($event['end']))
$this->duration = $event['start']->diff($event['end']);
// use Horde classes to compute recurring instances // use Horde classes to compute recurring instances
// TODO: replace with something that has less than 6'000 lines of code // TODO: replace with something that has less than 6'000 lines of code
require_once($this->cal->home . '/lib/Horde_Date_Recurrence.php'); require_once($this->cal->home . '/lib/Horde_Date_Recurrence.php');
$this->event = $event;
$this->engine = new Horde_Date_Recurrence($event['start']); $this->engine = new Horde_Date_Recurrence($event['start']);
$this->engine->fromRRule20(calendar::to_rrule($event['recurrence'])); $this->engine->fromRRule20(calendar::to_rrule($event['recurrence']));
if (is_array($event['recurrence']['EXDATE'])) { if (is_array($event['recurrence']['EXDATE'])) {
foreach ($event['recurrence']['EXDATE'] as $exdate) foreach ($event['recurrence']['EXDATE'] as $exdate)
$this->engine->addException(date('Y', $exdate), date('n', $exdate), date('j', $exdate)); $this->engine->addException($exdate->format('Y'), $exdate->format('n'), $exdate->format('j'));
} }
$this->tz_offset = $event['allday'] ? $this->cal->gmt_offset - date('Z') : 0;
$this->next = new Horde_Date($event['start'] + $this->tz_offset); # shift all-day times to server timezone because computation operates in local TZ
$this->dst_start = $this->next->format('I');
$this->hour = $this->next->hour;
} }
/** /**
* Get timestamp of the next occurence of this event * Get date/time of the next occurence of this event
* *
* @return mixed Unix timestamp or False if recurrence ended * @return mixed DateTime object or False if recurrence ended
*/ */
public function next_start() public function next_start()
{ {
@ -75,8 +75,8 @@ class calendar_recurrence
$next->hour = $this->hour; # fix time for all-day events $next->hour = $this->hour; # fix time for all-day events
$next->min = 0; $next->min = 0;
} }
# $dst_diff = ($this->dst_start - $next->format('I')) * 3600; # consider difference in daylight saving between base event and recurring instance
$time = $next->timestamp() - $this->tz_offset; $time = $next->toDateTime();
$this->next = $next; $this->next = $next;
} }

View file

@ -380,7 +380,7 @@ class calendar_ui
$this->cal->gettext(array('name' => $label, 'vars' => array('min' => $n % 60, 'hrs' => intval($n / 60)))))); $this->cal->gettext(array('name' => $label, 'vars' => array('min' => $n % 60, 'hrs' => intval($n / 60))))));
} }
return html::tag('ul', $attrib, join("\n", $items), html::$common_attrib); return html::tag('ul', $attrib + array('class' => 'toolbarmenu'), join("\n", $items), html::$common_attrib);
} }
/** /**

View file

@ -29,7 +29,9 @@ body.calendarmain #mainscreen {
} }
#datepicker { #datepicker {
margin-top: 12px; position: absolute;
top: 40px;
left: 0;
width: 100%; width: 100%;
min-height: 190px; min-height: 190px;
} }
@ -89,7 +91,7 @@ body.calendarmain #mainscreen {
position: absolute; position: absolute;
left: 254px; left: 254px;
width: 8px; width: 8px;
top: 37px; top: 40px;
bottom: 0; bottom: 0;
background: url(images/toggle.gif) 0 48% no-repeat transparent; background: url(images/toggle.gif) 0 48% no-repeat transparent;
cursor: pointer; cursor: pointer;
@ -241,17 +243,18 @@ pre {
#calendartoolbar { #calendartoolbar {
position: absolute; position: absolute;
top: -6px; top: -6px;
right: 0; left: 0;
height: 40px; height: 40px;
z-index: 200;
} }
#calendartoolbar a { #calendartoolbar a {
padding-right: 10px; padding-right: 10px;
} }
#quicksearchbar { body.calendarmain #quicksearchbar {
right: 4px; top: -7px;
right: 2px;
z-index: 200;
} }
body.calendarmain #searchmenulink { body.calendarmain #searchmenulink {
@ -870,7 +873,6 @@ span.edit-alarm-set {
} }
a.dropdown-link { a.dropdown-link {
color: #CC0000;
font-size: 12px; font-size: 12px;
text-decoration: none; text-decoration: none;
} }
@ -904,7 +906,6 @@ a.dropdown-link:after {
} }
.alarm-item div.alarm-actions a { .alarm-item div.alarm-actions a {
color: #CC0000;
margin-right: 0.8em; margin-right: 0.8em;
text-decoration: none; text-decoration: none;
} }
@ -1019,7 +1020,7 @@ span.spacer {
.fc-content { .fc-content {
position: absolute !important; position: absolute !important;
top: 38px; top: 40px;
left: 0; left: 0;
right: 0; right: 0;
bottom: 28px; bottom: 28px;
@ -1210,7 +1211,9 @@ div.fc-event-location {
background: -ms-linear-gradient(left, #e3f2f6 0, #d6eaf3 14px ,#d6eaf3 100%); background: -ms-linear-gradient(left, #e3f2f6 0, #d6eaf3 14px ,#d6eaf3 100%);
background: linear-gradient(left, #e3f2f6 0, #d6eaf3 14px, #d6eaf3 100%); background: linear-gradient(left, #e3f2f6 0, #d6eaf3 14px, #d6eaf3 100%);
border: 0; border: 0;
padding: 7px; border-bottom: 1px solid #ccc;
height: 18px;
padding: 8px 7px 3px 7px;
} }
.calendarmain .fc-view-table tr.fc-event td { .calendarmain .fc-view-table tr.fc-event td {

View file

@ -11,10 +11,11 @@
<div id="mainscreen"> <div id="mainscreen">
<div id="calendarsidebar"> <div id="calendarsidebar">
<div id="quicksearchbar"> <div id="calendartoolbar" class="toolbar">
<roundcube:object name="plugin.searchform" id="quicksearchbox" /> <roundcube:button command="addevent" type="link" class="button addevent disabled" classAct="button addevent" classSel="button addevent pressed" label="calendar.new_event" title="calendar.new_event" />
<a id="searchmenulink" class="iconbutton searchoptions" > </a> <roundcube:button command="print" type="link" class="button print disabled" classAct="button print" classSel="button print pressed" label="calendar.print" title="calendar.printtitle" />
<roundcube:button command="reset-search" id="searchreset" class="iconbutton reset" title="resetsearch" content=" " /> <roundcube:button command="export" type="link" class="button export disabled" classAct="button export" classSel="button export pressed" label="calendar.export" title="calendar.exporttitle" />
<roundcube:container name="toolbar" id="calendartoolbar" />
</div> </div>
<div id="datepicker" class="uibox"></div> <div id="datepicker" class="uibox"></div>
@ -31,11 +32,10 @@
</div> </div>
<div id="calendarsidebartoggle"></div> <div id="calendarsidebartoggle"></div>
<div id="calendartoolbar" class="toolbar"> <div id="quicksearchbar">
<roundcube:button command="addevent" type="link" class="button addevent disabled" classAct="button addevent" classSel="button addevent pressed" label="calendar.new_event" title="calendar.new_event" /> <roundcube:object name="plugin.searchform" id="quicksearchbox" />
<roundcube:button command="print" type="link" class="button print disabled" classAct="button print" classSel="button print pressed" label="calendar.print" title="calendar.printtitle" /> <a id="searchmenulink" class="iconbutton searchoptions" > </a>
<roundcube:button command="export" type="link" class="button export disabled" classAct="button export" classSel="button export pressed" label="calendar.export" title="calendar.exporttitle" /> <roundcube:button command="reset-search" id="searchreset" class="iconbutton reset" title="resetsearch" content=" " />
<roundcube:container name="toolbar" id="calendartoolbar" />
</div> </div>
<div id="calendar"> <div id="calendar">

View file

@ -1198,7 +1198,10 @@ class Horde_Date_Recurrence
'%04d%02d%02d'); '%04d%02d%02d');
$this->setRecurEnd(new Horde_Date(array('year' => $year, $this->setRecurEnd(new Horde_Date(array('year' => $year,
'month' => $month, 'month' => $month,
'mday' => $mday))); 'mday' => $mday,
'hour' => 23,
'min' => 59,
'sec' => 59)));
} }
if (isset($rdata['COUNT'])) { if (isset($rdata['COUNT'])) {
$this->setRecurCount($rdata['COUNT']); $this->setRecurCount($rdata['COUNT']);

View file

@ -55,7 +55,7 @@ class kolab_date_recurrence
$this->engine->fromRRule20($this->to_rrule($object['recurrence'])); // TODO: get that string directly from libkolabxml $this->engine->fromRRule20($this->to_rrule($object['recurrence'])); // TODO: get that string directly from libkolabxml
foreach ((array)$object['recurrence']['EXDATE'] as $exdate) foreach ((array)$object['recurrence']['EXDATE'] as $exdate)
$this->engine->addException(date('Y', $exdate), date('n', $exdate), date('j', $exdate)); $this->engine->addException($exdate->format('Y'), $exdate->format('n'), $exdate->format('j'));
$now = new DateTime('now', kolab_format::$timezone); $now = new DateTime('now', kolab_format::$timezone);
$this->tz_offset = $object['allday'] ? $now->getOffset() - date('Z') : 0; $this->tz_offset = $object['allday'] ? $now->getOffset() - date('Z') : 0;
@ -124,7 +124,7 @@ class kolab_date_recurrence
public function end($limit = 'now +1 year') public function end($limit = 'now +1 year')
{ {
if ($this->object['recurrence']['UNTIL']) if ($this->object['recurrence']['UNTIL'])
return $this->object['recurrence']['UNTIL']; return $this->object['recurrence']['UNTIL']->format('U');
$limit_time = strtotime($limit); $limit_time = strtotime($limit);
while ($next_start = $this->next_start(true)) { while ($next_start = $this->next_start(true)) {
@ -154,11 +154,11 @@ class kolab_date_recurrence
$k = strtoupper($k); $k = strtoupper($k);
switch ($k) { switch ($k) {
case 'UNTIL': case 'UNTIL':
$val = gmdate('Ymd\THis', $val); $val = $val->format('Ymd\THis');
break; break;
case 'EXDATE': case 'EXDATE':
foreach ((array)$val as $i => $ex) foreach ((array)$val as $i => $ex)
$val[$i] = gmdate('Ymd\THis', $ex); $val[$i] = $ex->format('Ymd\THis');
$val = join(',', (array)$val); $val = join(',', (array)$val);
break; break;
} }

View file

@ -136,6 +136,7 @@ class kolab_format_contact extends kolab_format
$this->obj->setUid($object['uid']); $this->obj->setUid($object['uid']);
$object['changed'] = new DateTime('now', self::$timezone); $object['changed'] = new DateTime('now', self::$timezone);
$this->obj->setLastModified(self::get_datetime($object['changed'], new DateTimeZone('UTC')));
// do the hard work of setting object values // do the hard work of setting object values
$nc = new NameComponents; $nc = new NameComponents;

View file

@ -50,6 +50,7 @@ class kolab_format_distributionlist extends kolab_format
$this->obj->setUid($object['uid']); $this->obj->setUid($object['uid']);
$object['changed'] = new DateTime('now', self::$timezone); $object['changed'] = new DateTime('now', self::$timezone);
$this->obj->setLastModified(self::get_datetime($object['changed'], new DateTimeZone('UTC')));
$this->obj->setName($object['name']); $this->obj->setName($object['name']);

View file

@ -229,7 +229,7 @@ class kolab_format_event extends kolab_format_xcal
if ($recurrence['range-type'] == 'number') if ($recurrence['range-type'] == 'number')
$rrule['COUNT'] = intval($recurrence['range']); $rrule['COUNT'] = intval($recurrence['range']);
else if ($recurrence['range-type'] == 'date') else if ($recurrence['range-type'] == 'date')
$rrule['UNTIL'] = $recurrence['range']; $rrule['UNTIL'] = date_create('@'.$recurrence['range']);
if ($recurrence['day']) { if ($recurrence['day']) {
$byday = array(); $byday = array();
@ -251,7 +251,7 @@ class kolab_format_event extends kolab_format_xcal
if ($recurrence['exclusion']) { if ($recurrence['exclusion']) {
foreach ((array)$recurrence['exclusion'] as $excl) foreach ((array)$recurrence['exclusion'] as $excl)
$rrule['EXDATE'][] = strtotime($excl . date(' H:i:s', $rec['start-date'])); // use time of event start $rrule['EXDATE'][] = date_create($excl . date(' H:i:s', $rec['start-date'])); // use time of event start
} }
} }

View file

@ -50,6 +50,7 @@ class kolab_format_journal extends kolab_format
$this->obj->setUid($object['uid']); $this->obj->setUid($object['uid']);
$object['changed'] = new DateTime('now', self::$timezone); $object['changed'] = new DateTime('now', self::$timezone);
$this->obj->setLastModified(self::get_datetime($object['changed'], new DateTimeZone('UTC')));
// TODO: set object propeties // TODO: set object propeties

View file

@ -50,6 +50,7 @@ class kolab_format_note extends kolab_format
$this->obj->setUid($object['uid']); $this->obj->setUid($object['uid']);
$object['changed'] = new DateTime('now', self::$timezone); $object['changed'] = new DateTime('now', self::$timezone);
$this->obj->setLastModified(self::get_datetime($object['changed'], new DateTimeZone('UTC')));
// TODO: set object propeties // TODO: set object propeties

View file

@ -144,7 +144,7 @@ abstract class kolab_format_xcal extends kolab_format
} }
else if ($until = self::php_datetime($rr->end())) { else if ($until = self::php_datetime($rr->end())) {
$until->setTime($object['start']->format('G'), $object['start']->format('i'), 0); $until->setTime($object['start']->format('G'), $object['start']->format('i'), 0);
$object['recurrence']['UNTIL'] = $until->format('U'); $object['recurrence']['UNTIL'] = $until;
} }
if (($byday = $rr->byday()) && $byday->size()) { if (($byday = $rr->byday()) && $byday->size()) {
@ -169,7 +169,7 @@ abstract class kolab_format_xcal extends kolab_format
if ($exceptions = $this->obj->exceptionDates()) { if ($exceptions = $this->obj->exceptionDates()) {
for ($i=0; $i < $exceptions->size(); $i++) { for ($i=0; $i < $exceptions->size(); $i++) {
if ($exdate = self::php_datetime($exceptions->get($i))) if ($exdate = self::php_datetime($exceptions->get($i)))
$object['recurrence']['EXDATE'][] = $exdate->format('U'); $object['recurrence']['EXDATE'][] = $exdate;
} }
} }
} }
@ -223,6 +223,7 @@ abstract class kolab_format_xcal extends kolab_format
$this->obj->setUid($object['uid']); $this->obj->setUid($object['uid']);
$object['changed'] = new DateTime('now', self::$timezone); $object['changed'] = new DateTime('now', self::$timezone);
$this->obj->setLastModified(self::get_datetime($object['changed'], new DateTimeZone('UTC')));
// increment sequence // increment sequence
$this->obj->setSequence($this->obj->sequence()+1); $this->obj->setSequence($this->obj->sequence()+1);

View file

@ -515,7 +515,7 @@ class kolab_storage_cache
// extend date range for recurring events // extend date range for recurring events
if ($object['recurrence']) { if ($object['recurrence']) {
$recurrence = new kolab_date_recurrence($object); $recurrence = new kolab_date_recurrence($object);
$sql_data['dtend'] = date('Y-m-d H:i:s', $recurrence->end() ?: strtotime('now +1 year')); $sql_data['dtend'] = date('Y-m-d 23:59:59', $recurrence->end() ?: strtotime('now +1 year'));
} }
} }
else if ($objtype == 'task') { else if ($objtype == 'task') {