diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php index 2666d3cb..ef396659 100644 --- a/plugins/calendar/calendar.php +++ b/plugins/calendar/calendar.php @@ -763,7 +763,7 @@ class calendar extends rcube_plugin case "new": // create UID for new event $event['uid'] = $this->generate_uid(); - $this->prepare_event($event, $action); + $this->write_preprocess($event, $action); if ($success = $this->driver->new_event($event)) { $event['id'] = $event['uid']; $this->cleanup_event($event); @@ -772,20 +772,20 @@ class calendar extends rcube_plugin break; case "edit": - $this->prepare_event($event, $action); + $this->write_preprocess($event, $action); if ($success = $this->driver->edit_event($event)) $this->cleanup_event($event); $reload = $success && ($event['recurrence'] || $event['_savemode'] || $event['_fromcalendar']) ? 2 : 1; break; case "resize": - $this->prepare_event($event, $action); + $this->write_preprocess($event, $action); $success = $this->driver->resize_event($event); $reload = $event['_savemode'] ? 2 : 1; break; case "move": - $this->prepare_event($event, $action); + $this->write_preprocess($event, $action); $success = $this->driver->move_event($event); $reload = $success && $event['_savemode'] ? 2 : 1; break; @@ -1327,8 +1327,10 @@ class calendar extends rcube_plugin private function _client_event($event, $addcss = false) { // compose a human readable strings for alarms_text and recurrence_text - if ($event['alarms']) - $event['alarms_text'] = libcalendaring::alarms_text($event['alarms']); + if ($event['valarms']) { + $event['alarms_text'] = libcalendaring::alarms_text($event['valarms']); + $event['valarms'] = libcalendaring::to_client_alarms($event['valarms']); + } if ($event['recurrence']) { $event['recurrence_text'] = $this->_recurrence_text($event['recurrence']); if ($event['recurrence']['UNTIL']) @@ -1555,7 +1557,7 @@ class calendar extends rcube_plugin /** * Prepares new/edited event properties before save */ - private function prepare_event(&$event, $action) + private function write_preprocess(&$event, $action) { // convert dates into DateTime objects in user's current timezone $event['start'] = new DateTime($event['start'], $this->timezone); @@ -1584,6 +1586,11 @@ class calendar extends rcube_plugin }, $event['recurrence']['RDATE']); } + // convert the submitted alarm values + if ($event['valarms']) { + $event['valarms'] = libcalendaring::from_client_alarms($event['valarms']); + } + $attachments = array(); $eventid = 'cal:'.$event['id']; if (is_array($_SESSION[self::SESSION_KEY]) && $_SESSION[self::SESSION_KEY]['id'] == $eventid) { diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js index 04ec3923..44681bad 100644 --- a/plugins/calendar/calendar_ui.js +++ b/plugins/calendar/calendar_ui.js @@ -81,7 +81,6 @@ function rcube_calendar_ui(settings) var date2unixtime = this.date2unixtime; var fromunixtime = this.fromunixtime; var parseISO8601 = this.parseISO8601; - var init_alarms_edit = this.init_alarms_edit; /*** private methods ***/ @@ -311,7 +310,7 @@ function rcube_calendar_ui(settings) if (event.recurrence && event.recurrence_text) $('#event-repeat').show().children('.event-text').html(Q(event.recurrence_text)); - if (event.alarms && event.alarms_text) + if (event.valarms && event.alarms_text) $('#event-alarm').show().children('.event-text').html(Q(event.alarms_text)); if (calendar.name) @@ -519,34 +518,10 @@ function rcube_calendar_ui(settings) else { allday.checked = false; } - + // set alarm(s) - // TODO: support multiple alarm entries - if (event.alarms || action != 'new') { - if (typeof event.alarms == 'string') - event.alarms = event.alarms.split(';'); - - var valarms = event.alarms || ['']; - for (var alarm, i=0; i < valarms.length; i++) { - alarm = String(valarms[i]).split(':'); - if (!alarm[1] && alarm[0]) alarm[1] = 'DISPLAY'; - $('#eventedit select.edit-alarm-type').val(alarm[1]); - - if (alarm[0].match(/@(\d+)/)) { - var ondate = fromunixtime(parseInt(RegExp.$1)); - $('#eventedit select.edit-alarm-offset').val('@'); - $('#eventedit input.edit-alarm-date').val($.fullCalendar.formatDate(ondate, settings['date_format'])); - $('#eventedit input.edit-alarm-time').val($.fullCalendar.formatDate(ondate, settings['time_format'])); - } - else if (alarm[0].match(/([-+])(\d+)([MHD])/)) { - $('#eventedit input.edit-alarm-value').val(RegExp.$2); - $('#eventedit select.edit-alarm-offset').val(''+RegExp.$1+RegExp.$3); - } - } - } - // set correct visibility by triggering onchange handlers - $('#eventedit select.edit-alarm-type, #eventedit select.edit-alarm-offset').change(); - + me.set_alarms_edit('#edit-alarms', action != 'new' && event.valarms && calendar.alarms ? event.valarms : []); + // enable/disable alarm property according to backend support $('#edit-alarms')[(calendar.alarms ? 'show' : 'hide')](); @@ -690,23 +665,12 @@ function rcube_calendar_ui(settings) sensitivity: sensitivity.val(), status: eventstatus.val(), recurrence: '', - alarms: '', + valarms: me.serialize_alarms('#edit-alarms'), attendees: event_attendees, deleted_attachments: rcmail.env.deleted_attachments, attachments: [] }; - // serialize alarm settings - // TODO: support multiple alarm entries - var alarm = $('#eventedit select.edit-alarm-type').val(); - if (alarm) { - var val, offset = $('#eventedit select.edit-alarm-offset').val(); - if (offset == '@') - data.alarms = '@' + date2unixtime(parse_datetime($('#eventedit input.edit-alarm-time').val(), $('#eventedit input.edit-alarm-date').val())) + ':' + alarm; - else if ((val = parseInt($('#eventedit input.edit-alarm-value').val())) && !isNaN(val) && val >= 0) - data.alarms = offset[0] + val + offset[1] + ':' + alarm; - } - // uploaded attachments list for (var i in rcmail.env.attachments) if (i.match(/^rcmfile(.+)/)) @@ -3260,7 +3224,7 @@ function rcube_calendar_ui(settings) }); // register events on alarm fields - init_alarms_edit('#eventedit'); + me.init_alarms_edit('#edit-alarms'); // toggle recurrence frequency forms $('#edit-recurrence-frequency').change(function(e){ diff --git a/plugins/calendar/drivers/database/SQL/mysql.initial.sql b/plugins/calendar/drivers/database/SQL/mysql.initial.sql index c45b3f2a..ed989be7 100644 --- a/plugins/calendar/drivers/database/SQL/mysql.initial.sql +++ b/plugins/calendar/drivers/database/SQL/mysql.initial.sql @@ -3,12 +3,11 @@ * * Plugin to add a calendar to Roundcube. * - * @version @package_version@ * @author Lazlo Westerhof * @author Thomas Bruederli - * @url http://rc-calendar.lazlo.me * @licence GNU AGPL * @copyright (c) 2010 Lazlo Westerhof - Netherlands + * @copyright (c) 2014 Kolab Systems AG * **/ diff --git a/plugins/calendar/drivers/database/SQL/postgres.initial.sql b/plugins/calendar/drivers/database/SQL/postgres.initial.sql index 007bbf29..21239c62 100644 --- a/plugins/calendar/drivers/database/SQL/postgres.initial.sql +++ b/plugins/calendar/drivers/database/SQL/postgres.initial.sql @@ -3,13 +3,12 @@ * * Plugin to add a calendar to RoundCube. * - * @version @package_version@ * @author Lazlo Westerhof * @author Albert Lee * @author Aleksander Machniak - * @url http://rc-calendar.lazlo.me * @licence GNU AGPL * @copyright (c) 2010 Lazlo Westerhof - Netherlands + * @copyright (c) 2014 Kolab Systems AG * **/ diff --git a/plugins/calendar/drivers/database/SQL/sqlite.initial.sql b/plugins/calendar/drivers/database/SQL/sqlite.initial.sql index 3d359073..078007de 100644 --- a/plugins/calendar/drivers/database/SQL/sqlite.initial.sql +++ b/plugins/calendar/drivers/database/SQL/sqlite.initial.sql @@ -3,13 +3,12 @@ * * Plugin to add a calendar to Roundcube. * - * @version @package_version@ * @author Lazlo Westerhof * @author Thomas Bruederli * @author Albert Lee - * @url http://rc-calendar.lazlo.me * @licence GNU AGPL * @copyright (c) 2010 Lazlo Westerhof - Netherlands + * @copyright (c) 2014 Kolab Systems AG * **/ diff --git a/plugins/calendar/drivers/database/database_driver.php b/plugins/calendar/drivers/database/database_driver.php index 77e49518..b4de23bb 100644 --- a/plugins/calendar/drivers/database/database_driver.php +++ b/plugins/calendar/drivers/database/database_driver.php @@ -459,10 +459,14 @@ class database_driver extends calendar_driver if (isset($event['allday'])) { $event['all_day'] = $event['allday'] ? 1 : 0; } - + // compute absolute time to notify the user $event['notifyat'] = $this->_get_notification($event); - + + if (is_array($event['valarms'])) { + $event['alarms'] = $this->serialize_alarms($event['valarms']); + } + // process event attendees $_attendees = ''; foreach ((array)$event['attendees'] as $attendee) { @@ -484,10 +488,10 @@ class database_driver extends calendar_driver */ private function _get_notification($event) { - if ($event['alarms'] && $event['start'] > new DateTime()) { + if ($event['valarms'] && $event['start'] > new DateTime()) { $alarm = libcalendaring::get_next_alarm($event); - if ($alarm['time'] && $alarm['action'] == 'DISPLAY') + if ($alarm['time'] && in_array($alarm['action'], $this->alarm_types)) return date('Y-m-d H:i:s', $alarm['time']); } @@ -877,7 +881,12 @@ class database_driver extends calendar_driver else { $event['attendees'] = array(); } - + + // decode serialized alarms + if ($event['alarms']) { + $event['valarms'] = $this->unserialize_alarms($event['alarms']); + } + unset($event['event_id'], $event['calendar_id'], $event['notifyat'], $event['all_day'], $event['_attachments']); return $event; } @@ -1088,4 +1097,48 @@ class database_driver extends calendar_driver return $this->rc->db->affected_rows($query); } + /** + * Helper method to serialize the list of alarms into a string + */ + private function serialize_alarms($valarms) + { + foreach ((array)$valarms as $i => $alarm) { + if ($alarm['trigger'] instanceof DateTime) { + $valarms[$i]['trigger'] = '@' . $alarm['trigger']->format('c'); + } + } + + return $valarms ? json_encode($valarms) : null; + } + + /** + * Helper method to decode a serialized list of alarms + */ + private function unserialize_alarms($alarms) + { + // decode json serialized alarms + if ($alarms && $alarms[0] == '[') { + $valarms = json_decode($alarms, true); + foreach ($valarms as $i => $alarm) { + if ($alarm['trigger'][0] == '@') { + try { + $valarms[$i]['trigger'] = new DateTime(substr($alarm['trigger'], 1)); + } + catch (Exception $e) { + unset($valarms[$i]); + } + } + } + } + // convert legacy alarms data + else if (strlen($alarms)) { + list($trigger, $action) = explode(':', $alarms, 2); + if ($trigger = libcalendaring::parse_alaram_value($trigger)) { + $valarms = array(array('action' => $action, 'trigger' => $trigger[3] ?: $trigger[0])); + } + } + + return $valarms; + } + } diff --git a/plugins/calendar/drivers/kolab/SQL/mysql.initial.sql b/plugins/calendar/drivers/kolab/SQL/mysql.initial.sql index f10d9029..88df960f 100644 --- a/plugins/calendar/drivers/kolab/SQL/mysql.initial.sql +++ b/plugins/calendar/drivers/kolab/SQL/mysql.initial.sql @@ -7,11 +7,11 @@ **/ CREATE TABLE IF NOT EXISTS `kolab_alarms` ( - `event_id` VARCHAR(255) NOT NULL, + `alarm_id` VARCHAR(255) NOT NULL, `user_id` int(10) UNSIGNED NOT NULL, `notifyat` DATETIME DEFAULT NULL, `dismissed` TINYINT(3) UNSIGNED NOT NULL DEFAULT '0', - PRIMARY KEY(`event_id`), + PRIMARY KEY(`alarm_id`), CONSTRAINT `fk_kolab_alarms_user_id` FOREIGN KEY (`user_id`) REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE ) /*!40000 ENGINE=INNODB */; diff --git a/plugins/calendar/drivers/kolab/SQL/mysql/2014041700.sql b/plugins/calendar/drivers/kolab/SQL/mysql/2014041700.sql new file mode 100644 index 00000000..9175b55d --- /dev/null +++ b/plugins/calendar/drivers/kolab/SQL/mysql/2014041700.sql @@ -0,0 +1 @@ +ALTER TABLE `kolab_alarms` CHANGE `event_id` `alarm_id` VARCHAR(255) NOT NULL; \ No newline at end of file diff --git a/plugins/calendar/drivers/kolab/SQL/postgres.initial.sql b/plugins/calendar/drivers/kolab/SQL/postgres.initial.sql index b8692400..e3ef9aa6 100644 --- a/plugins/calendar/drivers/kolab/SQL/postgres.initial.sql +++ b/plugins/calendar/drivers/kolab/SQL/postgres.initial.sql @@ -6,7 +6,7 @@ **/ CREATE TABLE IF NOT EXISTS kolab_alarms ( - event_id character varying(255) NOT NULL, + alarm_id character varying(255) NOT NULL, user_id integer NOT NULL REFERENCES users (user_id) ON DELETE CASCADE ON UPDATE CASCADE, notifyat timestamp without time zone DEFAULT NULL, diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php index caa4043f..6058dfb8 100644 --- a/plugins/calendar/drivers/kolab/kolab_driver.php +++ b/plugins/calendar/drivers/kolab/kolab_driver.php @@ -33,7 +33,7 @@ class kolab_driver extends calendar_driver public $freebusy = true; public $attachments = true; public $undelete = true; - public $alarm_types = array('DISPLAY'); + public $alarm_types = array('DISPLAY','AUDIO'); public $categoriesimmutable = true; private $rc; @@ -834,7 +834,7 @@ class kolab_driver extends calendar_driver $time = $slot + $interval; - $events = array(); + $candidates = array(); $query = array(array('tags', '=', 'x-has-alarms')); foreach ($this->calendars as $cid => $calendar) { // skip calendars with alarms disabled @@ -844,41 +844,48 @@ class kolab_driver extends calendar_driver foreach ($calendar->list_events($time, $time + 86400 * 365, null, 1, $query) as $e) { // add to list if alarm is set $alarm = libcalendaring::get_next_alarm($e); - if ($alarm && $alarm['time'] && $alarm['time'] <= $time && $alarm['action'] == 'DISPLAY') { - $id = $e['id']; - $events[$id] = $e; - $events[$id]['notifyat'] = $alarm['time']; + if ($alarm && $alarm['time'] && $alarm['time'] <= $time && in_array($alarm['action'], $this->alarm_types)) { + $id = $alarm['id']; // use alarm-id as primary identifier + $candidates[$id] = array( + 'id' => $id, + 'title' => $e['title'], + 'location' => $e['location'], + 'start' => $e['start'], + 'end' => $e['end'], + 'notifyat' => $alarm['time'], + 'action' => $alarm['action'], + ); } } } // get alarm information stored in local database - if (!empty($events)) { - $event_ids = array_map(array($this->rc->db, 'quote'), array_keys($events)); + if (!empty($candidates)) { + $alarm_ids = array_map(array($this->rc->db, 'quote'), array_keys($candidates)); $result = $this->rc->db->query(sprintf( "SELECT * FROM kolab_alarms - WHERE event_id IN (%s) AND user_id=?", - join(',', $event_ids), + WHERE alarm_id IN (%s) AND user_id=?", + join(',', $alarm_ids), $this->rc->db->now() ), $this->rc->user->ID ); while ($result && ($e = $this->rc->db->fetch_assoc($result))) { - $dbdata[$e['event_id']] = $e; + $dbdata[$e['alarm_id']] = $e; } } $alarms = array(); - foreach ($events as $id => $e) { - // skip dismissed + foreach ($candidates as $id => $alarm) { + // skip dismissed alarms if ($dbdata[$id]['dismissed']) continue; // snooze function may have shifted alarm time - $notifyat = $dbdata[$id]['notifyat'] ? strtotime($dbdata[$id]['notifyat']) : $e['notifyat']; + $notifyat = $dbdata[$id]['notifyat'] ? strtotime($dbdata[$id]['notifyat']) : $alarm['notifyat']; if ($notifyat <= $time) - $alarms[] = $e; + $alarms[] = $alarm; } return $alarms; @@ -889,13 +896,13 @@ class kolab_driver extends calendar_driver * * @see calendar_driver::dismiss_alarm() */ - public function dismiss_alarm($event_id, $snooze = 0) + public function dismiss_alarm($alarm_id, $snooze = 0) { // delete old alarm entry $this->rc->db->query( "DELETE FROM kolab_alarms - WHERE event_id=? AND user_id=?", - $event_id, + WHERE alarm_id=? AND user_id=?", + $alarm_id, $this->rc->user->ID ); @@ -904,9 +911,9 @@ class kolab_driver extends calendar_driver $query = $this->rc->db->query( "INSERT INTO kolab_alarms - (event_id, user_id, dismissed, notifyat) + (alarm_id, user_id, dismissed, notifyat) VALUES(?, ?, ?, ?)", - $event_id, + $alarm_id, $this->rc->user->ID, $snooze > 0 ? 0 : 1, $notifyat diff --git a/plugins/calendar/skins/classic/calendar.css b/plugins/calendar/skins/classic/calendar.css index 6c7ca146..40350fa7 100644 --- a/plugins/calendar/skins/classic/calendar.css +++ b/plugins/calendar/skins/classic/calendar.css @@ -535,6 +535,44 @@ td.topalign { vertical-align: top; } +#eventedit .edit-alarm-item { + position: relative; + padding-right: 30px; + margin-bottom: 2px; +} + +#eventedit .edit-alarm-buttons { + position: absolute; + top: 2px; + right: 0; +} + +#eventedit .edit-alarm-buttons a.iconlink { + display: none; + width: 18px; + height: 17px; + padding: 1px; + text-indent: -5000px; + overflow: hidden; +} + +#eventedit .edit-alarm-buttons a.add-alarm { + background: url(images/plus.png) 1px 1px no-repeat; +} + +#eventedit .edit-alarm-buttons a.delete-alarm { + background: url(images/delete.png) 1px 1px no-repeat; +} + +#eventedit .edit-alarm-buttons a.delete-alarm, +#eventedit .first .edit-alarm-buttons a.add-alarm { + display: inline-block; +} + +#eventedit .first .edit-alarm-buttons a.delete-alarm { + display: none; +} + #eventedit label.weekday, #eventedit label.monthday { min-width: 3em; diff --git a/plugins/calendar/skins/classic/images/delete.png b/plugins/calendar/skins/classic/images/delete.png new file mode 100644 index 00000000..553ae437 Binary files /dev/null and b/plugins/calendar/skins/classic/images/delete.png differ diff --git a/plugins/calendar/skins/classic/images/plus.png b/plugins/calendar/skins/classic/images/plus.png new file mode 100644 index 00000000..1a35013e Binary files /dev/null and b/plugins/calendar/skins/classic/images/plus.png differ diff --git a/plugins/calendar/skins/classic/templates/eventedit.html b/plugins/calendar/skins/classic/templates/eventedit.html index 7e7170d2..7678d0cf 100644 --- a/plugins/calendar/skins/classic/templates/eventedit.html +++ b/plugins/calendar/skins/classic/templates/eventedit.html @@ -41,8 +41,14 @@
- - +
+ + + + + + - + +
diff --git a/plugins/calendar/skins/larry/templates/eventedit.html b/plugins/calendar/skins/larry/templates/eventedit.html index 85b3a771..208579ff 100644 --- a/plugins/calendar/skins/larry/templates/eventedit.html +++ b/plugins/calendar/skins/larry/templates/eventedit.html @@ -37,8 +37,14 @@
- - +
+ + + + + + - + +
diff --git a/plugins/libcalendaring/libcalendaring.js b/plugins/libcalendaring/libcalendaring.js index 3808de16..aa9d227a 100644 --- a/plugins/libcalendaring/libcalendaring.js +++ b/plugins/libcalendaring/libcalendaring.js @@ -311,9 +311,87 @@ function rcube_libcalendaring(settings) $(this).parent().find('.edit-alarm-value').prop('disabled', mode == 'show'); }); - $(prefix+' .edit-alarm-date').datepicker(datepicker_settings); + $(prefix+' .edit-alarm-date').removeClass('hasDatepicker').removeAttr('id').datepicker(datepicker_settings); + + $(prefix).on('click', 'a.delete-alarm', function(e){ + if ($(this).closest('.edit-alarm-item').siblings().length > 0) { + $(this).closest('.edit-alarm-item').remove(); + } + return false; + }); + + $(prefix).on('click', 'a.add-alarm', function(e){ + var i = $(this).closest('.edit-alarm-item').siblings().length + 1; + var item = $(this).closest('.edit-alarm-item').clone(false) + .removeClass('first') + .appendTo(prefix); + + me.init_alarms_edit(prefix + ' .edit-alarm-item:eq(' + i + ')'); + $('select.edit-alarm-type, select.edit-alarm-offset', item).change(); + return false; + }); } + this.set_alarms_edit = function(prefix, valarms) + { + $(prefix + ' .edit-alarm-item:gt(0)').remove(); + + var i, alarm, domnode, val, offset; + for (i=0; i < valarms.length; i++) { + alarm = valarms[i]; + if (!alarm.action) + alarm.action = 'DISPLAY'; + + if (i == 0) { + domnode = $(prefix + ' .edit-alarm-item').eq(0); + } + else { + domnode = $(prefix + ' .edit-alarm-item').eq(0).clone(false).removeClass('first').appendTo(prefix); + this.init_alarms_edit(prefix + ' .edit-alarm-item:eq(' + i + ')'); + } + + $('select.edit-alarm-type', domnode).val(alarm.action); + + if (String(alarm.trigger).match(/@(\d+)/)) { + var ondate = this.fromunixtime(parseInt(RegExp.$1)); + $('select.edit-alarm-offset', domnode).val('@'); + $('input.edit-alarm-value', domnode).val(''); + $('input.edit-alarm-date', domnode).val(this.format_datetime(ondate, 1)); + $('input.edit-alarm-time', domnode).val(this.format_datetime(ondate, 2)); + } + else if (String(alarm.trigger).match(/([-+])(\d+)([MHDS])/)) { + val = RegExp.$2; offset = ''+RegExp.$1+RegExp.$3; + $('input.edit-alarm-value', domnode).val(val); + $('select.edit-alarm-offset', domnode).val(offset); + } + } + + // set correct visibility by triggering onchange handlers + $(prefix + ' select.edit-alarm-type, ' + prefix + ' select.edit-alarm-offset').change(); + }; + + this.serialize_alarms = function(prefix) + { + var valarms = []; + + $(prefix + ':visible .edit-alarm-item').each(function(i, elem){ + var val, offset, alarm = { action: $('select.edit-alarm-type', elem).val() }; + if (alarm.action) { + offset = $('select.edit-alarm-offset', elem).val(); + if (offset == '@') { + alarm.trigger = '@' + me.date2unixtime(me.parse_datetime($('input.edit-alarm-time', elem).val(), $('input.edit-alarm-date', elem).val())); + } + else if (!isNaN((val = parseInt($('input.edit-alarm-value', elem).val()))) && val >= 0) { + alarm.trigger = offset[0] + val + offset[1]; + } + + valarms.push(alarm); + } + }); + + return valarms; + }; + /***** Alarms handling *****/ diff --git a/plugins/libcalendaring/libcalendaring.php b/plugins/libcalendaring/libcalendaring.php index edc0ddee..e71509c7 100644 --- a/plugins/libcalendaring/libcalendaring.php +++ b/plugins/libcalendaring/libcalendaring.php @@ -345,25 +345,89 @@ class libcalendaring extends rcube_plugin public static function parse_alaram_value($val) { if ($val[0] == '@') { - return array(substr($val, 1)); + return array(new DateTime($val)); } - else if (preg_match('/([+-])P?(T?\d+[HMSDW])+/', $val, $m) && preg_match_all('/T?(\d+)([HMSDW])/', $val, $m2, PREG_SET_ORDER)) { + else if (preg_match('/([+-]?)P?(T?\d+[HMSDW])+/', $val, $m) && preg_match_all('/T?(\d+)([HMSDW])/', $val, $m2, PREG_SET_ORDER)) { + if ($m[1] == '') + $m[1] = '+'; foreach ($m2 as $seg) { + $prefix = $seg[2] == 'D' || $seg[2] == 'W' ? 'P' : 'PT'; if ($seg[1] > 0) { // ignore zero values - return array($seg[1], $m[1].$seg[2], $m[1].$seg[1].$seg[2]); + return array($seg[1], $m[1].$seg[2], $m[1].$seg[1].$seg[2], $m[1].$prefix.$seg[1].$seg[2]); } } + + // return zero value nevertheless + return array($seg[1], $m[1].$seg[2], $m[1].$seg[1].$seg[2], $m[1].$prefix.$seg[1].$seg[2]); } return false; } + /** + * Convert the alarms list items to be processed on the client + */ + public static function to_client_alarms($valarms) + { + return array_map(function($alarm){ + if ($alarm['trigger'] instanceof DateTime) { + $alarm['trigger'] = '@' . $alarm['trigger']->format('U'); + } + else if ($trigger = self::parse_alaram_value($alarm['trigger'])) { + $alarm['trigger'] = $trigger[2]; + } + return $alarm; + }, (array)$valarms); + } + + /** + * Process the alarms values submitted by the client + */ + public static function from_client_alarms($valarms) + { + return array_map(function($alarm){ + if ($alarm['trigger'][0] == '@') { + try { $alarm['trigger'] = new DateTime($alarm['trigger']); } + catch (Exception $e) { /* handle this ? */ } + } + else if ($trigger = libcalendaring::parse_alaram_value($alarm['trigger'])) { + $alarm['trigger'] = $trigger[3]; + } + return $alarm; + }, (array)$valarms); + } + /** * Render localized text for alarm settings */ - public static function alarms_text($alarm) + public static function alarms_text($alarms) { - list($trigger, $action) = explode(':', $alarm); + if (is_array($alarms) && is_array($alarms[0])) { + $texts = array(); + foreach ($alarms as $alarm) { + if ($text = self::alarm_text($alarm)) + $texts[] = $text; + } + + return join(', ', $texts); + } + else { + return self::alarm_text($alarms); + } + } + + /** + * Render localized text for a single alarm property + */ + public static function alarm_text($alarm) + { + if (is_string($alarm)) { + list($trigger, $action) = explode(':', $alarm); + } + else { + $trigger = $alarm['trigger']; + $action = $alarm['action']; + } $text = ''; $rcube = rcube::get_instance(); @@ -375,19 +439,33 @@ class libcalendaring extends rcube_plugin case 'DISPLAY': $text = $rcube->gettext('libcalendaring.alarmdisplay'); break; + case 'AUDIO': + $text = $rcube->gettext('libcalendaring.alarmaudio'); + break; } - if (preg_match('/@(\d+)/', $trigger, $m)) { + if ($trigger instanceof DateTime) { + $text .= ' ' . $rcube->gettext(array( + 'name' => 'libcalendaring.alarmat', + 'vars' => array('datetime' => $rcube->format_date($trigger)) + )); + } + else if (preg_match('/@(\d+)/', $trigger, $m)) { $text .= ' ' . $rcube->gettext(array( 'name' => 'libcalendaring.alarmat', 'vars' => array('datetime' => $rcube->format_date($m[1])) )); } else if ($val = self::parse_alaram_value($trigger)) { - $text .= ' ' . intval($val[0]) . ' ' . $rcube->gettext('libcalendaring.trigger' . $val[1]); + // TODO: for all-day events say 'on date of event at XX' ? + if ($val[0] == 0) + $text .= ' ' . $rcube->gettext('libcalendaring.triggerattime'); + else + $text .= ' ' . intval($val[0]) . ' ' . $rcube->gettext('libcalendaring.trigger' . $val[1]); } - else + else { return false; + } return $text; } @@ -400,53 +478,78 @@ class libcalendaring extends rcube_plugin */ public static function get_next_alarm($rec, $type = 'event') { - if (!$rec['alarms'] || $rec['cancelled'] || $rec['status'] == 'CANCELLED') + if (!($rec['valarms'] || $rec['alarms']) || $rec['cancelled'] || $rec['status'] == 'CANCELLED') return null; if ($type == 'task') { $timezone = self::get_instance()->timezone; - if ($rec['date']) - $rec['start'] = new DateTime($rec['date'] . ' ' . ($rec['time'] ?: '12:00'), $timezone); if ($rec['startdate']) - $rec['end'] = new DateTime($rec['startdate'] . ' ' . ($rec['starttime'] ?: '12:00'), $timezone); + $rec['start'] = new DateTime($rec['startdate'] . ' ' . ($rec['starttime'] ?: '12:00'), $timezone); + if ($rec['date']) + $rec[($rec['start'] ? 'end' : 'start')] = new DateTime($rec['date'] . ' ' . ($rec['time'] ?: '12:00'), $timezone); } if (!$rec['end']) $rec['end'] = $rec['start']; - - // TODO: handle multiple alarms (currently not supported) - list($trigger, $action) = explode(':', $rec['alarms'], 2); - - $notify = self::parse_alaram_value($trigger); - if (!empty($notify[1])){ // offset - $mult = 1; - switch ($notify[1]) { - case '-S': $mult = -1; break; - case '+S': $mult = 1; break; - case '-M': $mult = -60; break; - case '+M': $mult = 60; break; - case '-H': $mult = -3600; break; - case '+H': $mult = 3600; break; - case '-D': $mult = -86400; break; - case '+D': $mult = 86400; break; - case '-W': $mult = -604800; break; - case '+W': $mult = 604800; break; + // support legacy format + if (!$rec['valarms']) { + list($trigger, $action) = explode(':', $rec['alarms'], 2); + if ($alarm = self::parse_alaram_value($trigger)) { + $rec['valarms'] = array(array('action' => $action, 'trigger' => $alarm[3] ?: $alarm[0])); } - $offset = $notify[0] * $mult; - $refdate = $mult > 0 ? $rec['end'] : $rec['start']; - - // abort of no reference date is available to compute notification time - if (!is_a($refdate, 'DateTime')) - return null; - - $notify_at = $refdate->format('U') + $offset; - } - else { // absolute timestamp - $notify_at = $notify[0]; } - return array('time' => $notify_at, 'action' => $action ? strtoupper($action) : 'DISPLAY'); + $expires = new DateTime('now - 12 hours'); + $alarm_id = $rec['id']; // alarm ID eq. record ID by default to keep backwards compatibility + + // handle multiple alarms + $notify_at = null; + foreach ($rec['valarms'] as $alarm) { + $notify_time = null; + + if ($alarm['trigger'] instanceof DateTime) { + $notify_time = $alarm['trigger']; + } + else if (is_string($alarm['trigger'])) { + $refdate = $alarm['trigger'][0] == '+' ? $rec['end'] : $rec['start']; + + // abort if no reference date is available to compute notification time + if (!is_a($refdate, 'DateTime')) + continue; + + // TODO: for all-day events, take start @ 00:00 as reference date ? + + try { + $interval = new DateInterval(trim($alarm['trigger'], '+-')); + $interval->invert = $alarm['trigger'][0] != '+'; + $notify_time = clone $refdate; + $notify_time->add($interval); + } + catch (Exception $e) { + rcube::raise_error($e, true); + continue; + } + } + + if ($notify_time && (!$notify_at || ($notify_time < $notify_at && $notify_time > $expires))) { + $notify_at = $notify_time; + $action = $alarm['action']; + $alarm_prop = $alarm; + + // generate a unique alarm ID if multiple alarms are set + if (count($rec['valarms']) > 1) { + $alarm_id = substr(md5($rec['id']), 0, 16) . '-' . $notify_at->format('Ymd\THis'); + } + } + } + + return !$notify_at ? null : array( + 'time' => $notify_at->format('U'), + 'action' => $action ? strtoupper($action) : 'DISPLAY', + 'id' => $alarm_id, + 'prop' => $alarm_prop, + ); } /** diff --git a/plugins/libcalendaring/libvcalendar.php b/plugins/libcalendaring/libvcalendar.php index f7d7ccc1..1dda548d 100644 --- a/plugins/libcalendaring/libvcalendar.php +++ b/plugins/libcalendaring/libvcalendar.php @@ -1025,9 +1025,10 @@ class libvcalendar implements Iterator $va = VObject\Component::create('VALARM'); list($trigger, $va->action) = explode(':', $event['alarms']); $val = libcalendaring::parse_alaram_value($trigger); - $period = $val[1] && preg_match('/[HMS]$/', $val[1]) ? 'PT' : 'P'; - if ($val[1]) $va->add('TRIGGER', preg_replace('/^([-+])P?T?(.+)/', "\\1$period\\2", $trigger)); - else $va->add('TRIGGER', gmdate('Ymd\THis\Z', $val[0]), array('VALUE' => 'DATE-TIME')); + if ($val[3]) + $va->add('TRIGGER', $val[3]); + else if ($val[0] instanceof DateTime) + $va->add(self::datetime_prop('TRIGGER', $val[0])); $ve->add($va); } diff --git a/plugins/libcalendaring/localization/en_US.inc b/plugins/libcalendaring/localization/en_US.inc index 5eecd293..c3159ffd 100644 --- a/plugins/libcalendaring/localization/en_US.inc +++ b/plugins/libcalendaring/localization/en_US.inc @@ -4,8 +4,10 @@ $labels = array(); $labels['alarmemail'] = 'Send Email'; $labels['alarmdisplay'] = 'Show message'; +$labels['alarmaudio'] = 'Play sound'; $labels['alarmdisplayoption'] = 'Message'; $labels['alarmemailoption'] = 'Email'; +$labels['alarmaudiooption'] = 'Sound'; $labels['alarmat'] = 'at $datetime'; $labels['trigger@'] = 'on date'; $labels['trigger-M'] = 'minutes before'; @@ -14,6 +16,7 @@ $labels['trigger-D'] = 'days before'; $labels['trigger+M'] = 'minutes after'; $labels['trigger+H'] = 'hours after'; $labels['trigger+D'] = 'days after'; +$labels['triggerattime'] = 'at time'; $labels['addalarm'] = 'add alarm'; $labels['alarmtitle'] = 'Upcoming events'; diff --git a/plugins/libcalendaring/skins/larry/libcal.css b/plugins/libcalendaring/skins/larry/libcal.css index 62d29479..04fb2f14 100644 --- a/plugins/libcalendaring/skins/larry/libcal.css +++ b/plugins/libcalendaring/skins/larry/libcal.css @@ -1,7 +1,7 @@ /** * Roundcube libcalendaring plugin styles for skin "Larry" * - * Copyright (c) 2012, Kolab Systems AG + * Copyright (c) 2012-2014, Kolab Systems AG * * The contents are subject to the Creative Commons Attribution-ShareAlike * License. It is allowed to copy, distribute, transmit and to adapt the work @@ -75,4 +75,39 @@ a.reply-comment-toggle { .popup textarea.itip-comment { width: 98%; -} \ No newline at end of file +} + +.edit-alarm-item { + position: relative; + padding-right: 30px; + margin-bottom: 0.2em; +} + +.edit-alarm-buttons { + position: absolute; + top: 1px; + right: 0; +} + +.edit-alarm-buttons a.iconlink { + display: none; + width: 18px; + height: 17px; + padding: 1px; + text-indent: -5000px; + overflow: hidden; +} + +.edit-alarm-buttons a.delete-alarm { + background-position: -7px -377px; +} + +.edit-alarm-buttons a.delete-alarm, +.edit-alarm-item.first .edit-alarm-buttons a.add-alarm { + display: inline-block; +} + +.edit-alarm-item.first .edit-alarm-buttons a.delete-alarm { + display: none; +} + diff --git a/plugins/libcalendaring/tests/libvcalendar.php b/plugins/libcalendaring/tests/libvcalendar.php index a20844e2..1ca81bb7 100644 --- a/plugins/libcalendaring/tests/libvcalendar.php +++ b/plugins/libcalendaring/tests/libvcalendar.php @@ -182,7 +182,7 @@ class libvcalendar_test extends PHPUnit_Framework_TestCase $this->assertEquals('-H', $alarm[1], "Alarm unit"); $this->assertEquals('DISPLAY', $event['valarms'][0]['action'], "Full alarm item (action)"); - $this->assertEquals('-PT12H', $event['valarms'][0]['trigger'], "Full alarm item (trigger)"); + $this->assertEquals('-PT12H', $event['valarms'][0]['trigger'], "Full alarm item (trigger)"); // alarm trigger with 0 values $events = $ical->import_from_file(__DIR__ . '/resources/alarms.ics', 'UTF-8'); @@ -193,6 +193,7 @@ class libvcalendar_test extends PHPUnit_Framework_TestCase $this->assertEquals('30', $alarm[0], "Alarm value"); $this->assertEquals('-M', $alarm[1], "Alarm unit"); $this->assertEquals('-30M', $alarm[2], "Alarm string"); + $this->assertEquals('-PT30M', $alarm[3], "Unified alarm string (stripped zero-values)"); $this->assertEquals('DISPLAY', $event['valarms'][0]['action'], "First alarm action"); $this->assertEquals('This is the first event reminder', $event['valarms'][0]['description'], "First alarm text"); diff --git a/plugins/tasklist/drivers/database/tasklist_database_driver.php b/plugins/tasklist/drivers/database/tasklist_database_driver.php index 7d4c0f11..f40d5048 100644 --- a/plugins/tasklist/drivers/database/tasklist_database_driver.php +++ b/plugins/tasklist/drivers/database/tasklist_database_driver.php @@ -482,6 +482,12 @@ class tasklist_database_driver extends tasklist_driver if (!$rec['parent_id']) unset($rec['parent_id']); + // decode serialized alarms + if ($rec['alarms']) { + $rec['valarms'] = $this->unserialize_alarms($rec['alarms']); + unset($rec['alarms']); + } + unset($rec['task_id'], $rec['tasklist_id'], $rec['created']); return $rec; } @@ -500,6 +506,10 @@ class tasklist_database_driver extends tasklist_driver if (!$this->lists[$list_id] || $this->lists[$list_id]['readonly']) return false; + if (is_array($prop['valarms'])) { + $prop['alarms'] = $this->serialize_alarms($prop['valarms']); + } + foreach (array('parent_id', 'date', 'time', 'startdate', 'starttime', 'alarms') as $col) { if (empty($prop[$col])) $prop[$col] = null; @@ -542,6 +552,10 @@ class tasklist_database_driver extends tasklist_driver */ public function edit_task($prop) { + if (is_array($prop['valarms'])) { + $prop['alarms'] = $this->serialize_alarms($prop['valarms']); + } + $sql_set = array(); foreach (array('title', 'description', 'flagged', 'complete') as $col) { if (isset($prop[$col])) @@ -655,14 +669,58 @@ class tasklist_database_driver extends tasklist_driver */ private function _get_notification($task) { - if ($task['alarms'] && $task['complete'] < 1 || strpos($task['alarms'], '@') !== false) { + if ($task['valarms'] && $task['complete'] < 1) { $alarm = libcalendaring::get_next_alarm($task, 'task'); - if ($alarm['time'] && $alarm['action'] == 'DISPLAY') + if ($alarm['time'] && in_array($alarm['action'], $this->alarm_types)) return date('Y-m-d H:i:s', $alarm['time']); } return null; } + /** + * Helper method to serialize the list of alarms into a string + */ + private function serialize_alarms($valarms) + { + foreach ((array)$valarms as $i => $alarm) { + if ($alarm['trigger'] instanceof DateTime) { + $valarms[$i]['trigger'] = '@' . $alarm['trigger']->format('c'); + } + } + + return $valarms ? json_encode($valarms) : null; + } + + /** + * Helper method to decode a serialized list of alarms + */ + private function unserialize_alarms($alarms) + { + // decode json serialized alarms + if ($alarms && $alarms[0] == '[') { + $valarms = json_decode($alarms, true); + foreach ($valarms as $i => $alarm) { + if ($alarm['trigger'][0] == '@') { + try { + $valarms[$i]['trigger'] = new DateTime(substr($alarm['trigger'], 1)); + } + catch (Exception $e) { + unset($valarms[$i]); + } + } + } + } + // convert legacy alarms data + else if (strlen($alarms)) { + list($trigger, $action) = explode(':', $alarms, 2); + if ($trigger = libcalendaring::parse_alaram_value($trigger)) { + $valarms = array(array('action' => $action, 'trigger' => $trigger[3] ?: $trigger[0])); + } + } + + return $valarms; + } + } diff --git a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php index b2d3d56b..56dc9553 100644 --- a/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php +++ b/plugins/tasklist/drivers/kolab/tasklist_kolab_driver.php @@ -28,7 +28,7 @@ class tasklist_kolab_driver extends tasklist_driver public $alarms = false; public $attachments = true; public $undelete = false; // task undelete action - public $alarm_types = array('DISPLAY'); + public $alarm_types = array('DISPLAY','AUDIO'); private $rc; private $plugin; @@ -477,7 +477,7 @@ class tasklist_kolab_driver extends tasklist_driver $time = $slot + $interval; - $tasks = array(); + $candidates = array(); $query = array(array('tags', '=', 'x-has-alarms'), array('tags', '!=', 'x-complete')); foreach ($this->lists as $lid => $list) { // skip lists with alarms disabled @@ -486,40 +486,46 @@ class tasklist_kolab_driver extends tasklist_driver $folder = $this->folders[$lid]; foreach ($folder->select($query) as $record) { - if (!$record['alarms'] || $record['status'] == 'COMPLETED' || $record['complete'] == 100) // don't trust query :-) + if (!($record['valarms'] || $record['alarms']) || $record['status'] == 'COMPLETED' || $record['complete'] == 100) // don't trust query :-) continue; $task = $this->_to_rcube_task($record); // add to list if alarm is set $alarm = libcalendaring::get_next_alarm($task, 'task'); - if ($alarm && $alarm['time'] && $alarm['time'] <= $time && $alarm['action'] == 'DISPLAY') { - $id = $task['id']; - $tasks[$id] = $task; - $tasks[$id]['notifyat'] = $alarm['time']; + if ($alarm && $alarm['time'] && $alarm['time'] <= $time && in_array($alarm['action'], $this->alarm_types)) { + $id = $alarm['id']; // use alarm-id as primary identifier + $candidates[$id] = array( + 'id' => $id, + 'title' => $task['title'], + 'date' => $task['date'], + 'time' => $task['time'], + 'notifyat' => $alarm['time'], + 'action' => $alarm['action'], + ); } } } // get alarm information stored in local database - if (!empty($tasks)) { - $task_ids = array_map(array($this->rc->db, 'quote'), array_keys($tasks)); + if (!empty($candidates)) { + $alarm_ids = array_map(array($this->rc->db, 'quote'), array_keys($candidates)); $result = $this->rc->db->query(sprintf( "SELECT * FROM kolab_alarms - WHERE event_id IN (%s) AND user_id=?", - join(',', $task_ids), + WHERE alarm_id IN (%s) AND user_id=?", + join(',', $alarm_ids), $this->rc->db->now() ), $this->rc->user->ID ); while ($result && ($rec = $this->rc->db->fetch_assoc($result))) { - $dbdata[$rec['event_id']] = $rec; + $dbdata[$rec['alarm_id']] = $rec; } } $alarms = array(); - foreach ($tasks as $id => $task) { + foreach ($candidates as $id => $task) { // skip dismissed if ($dbdata[$id]['dismissed']) continue; @@ -545,7 +551,7 @@ class tasklist_kolab_driver extends tasklist_driver // delete old alarm entry $this->rc->db->query( "DELETE FROM kolab_alarms - WHERE event_id=? AND user_id=?", + WHERE alarm_id=? AND user_id=?", $id, $this->rc->user->ID ); @@ -555,7 +561,7 @@ class tasklist_kolab_driver extends tasklist_driver $query = $this->rc->db->query( "INSERT INTO kolab_alarms - (event_id, user_id, dismissed, notifyat) + (alarm_id, user_id, dismissed, notifyat) VALUES(?, ?, ?, ?)", $id, $this->rc->user->ID, @@ -599,7 +605,10 @@ class tasklist_kolab_driver extends tasklist_driver $task['changed'] = $record['dtstamp']; } - if ($record['alarms']) { + if ($record['valarms']) { + $task['valarms'] = $record['valarms']; + } + else if ($record['alarms']) { $task['alarms'] = $record['alarms']; } diff --git a/plugins/tasklist/skins/larry/templates/taskedit.html b/plugins/tasklist/skins/larry/templates/taskedit.html index 1c9aa4ed..7dc0b400 100644 --- a/plugins/tasklist/skins/larry/templates/taskedit.html +++ b/plugins/tasklist/skins/larry/templates/taskedit.html @@ -32,8 +32,14 @@
- - +
+ + + + + + - + +
diff --git a/plugins/tasklist/tasklist.js b/plugins/tasklist/tasklist.js index cba90780..6fb3eb3b 100644 --- a/plugins/tasklist/tasklist.js +++ b/plugins/tasklist/tasklist.js @@ -107,7 +107,6 @@ function rcube_tasklist_ui(settings) var parse_datetime = this.parse_datetime; var date2unixtime = this.date2unixtime; var fromunixtime = this.fromunixtime; - var init_alarms_edit = this.init_alarms_edit; /** * initialize the tasks UI @@ -380,7 +379,7 @@ function rcube_tasklist_ui(settings) }); // register events on alarm fields - init_alarms_edit('#taskedit'); + me.init_alarms_edit('#taskedit-alarms'); $('#taskedit-date, #taskedit-startdate').datepicker(datepicker_settings); @@ -1169,7 +1168,7 @@ function rcube_tasklist_ui(settings) if (rcmail.busy || !list.editable || (action == 'edit' && (!rec || rec.readonly))) return false; - me.selected_task = $.extend({ alarms:'' }, rec); // clone task object + me.selected_task = $.extend({ valarms:[] }, rec); // clone task object rec = me.selected_task; // assign temporary id @@ -1210,29 +1209,7 @@ function rcube_tasklist_ui(settings) }); // set alarm(s) - if (rec.alarms || action != 'new') { - var valarms = (typeof rec.alarms == 'string' ? rec.alarms.split(';') : rec.alarms) || ['']; - for (var alarm, i=0; i < valarms.length; i++) { - alarm = String(valarms[i]).split(':'); - if (!alarm[1] && alarm[0]) alarm[1] = 'DISPLAY'; - $('#taskedit select.edit-alarm-type').val(alarm[1]); - - if (alarm[0].match(/@(\d+)/)) { - var ondate = fromunixtime(parseInt(RegExp.$1)); - $('#taskedit select.edit-alarm-offset').val('@'); - $('#taskedit input.edit-alarm-date').val(me.format_datetime(ondate, 1)); - $('#taskedit input.edit-alarm-time').val(me.format_datetime(ondate, 2)); - } - else if (alarm[0].match(/([-+])(\d+)([MHD])/)) { - $('#taskedit input.edit-alarm-value').val(RegExp.$2); - $('#taskedit select.edit-alarm-offset').val(''+RegExp.$1+RegExp.$3); - } - - break; // only one alarm is currently supported - } - } - // set correct visibility by triggering onchange handlers - $('#taskedit select.edit-alarm-type, #taskedit select.edit-alarm-offset').change(); + me.set_alarms_edit('#taskedit-alarms', action != 'new' && rec.valarms ? rec.valarms : []); // attachments rcmail.enable_command('remove-attachment', list.editable); @@ -1263,6 +1240,7 @@ function rcube_tasklist_ui(settings) }); me.selected_task.tags = []; me.selected_task.attachments = []; + me.selected_task.valarms = me.serialize_alarms('#taskedit-alarms'); // do some basic input validation if (!me.selected_task.title || !me.selected_task.title.length) { @@ -1289,16 +1267,6 @@ function rcube_tasklist_ui(settings) me.selected_task.tags.push(newtag); } - // serialize alarm settings - var alarm = $('#taskedit select.edit-alarm-type').val(); - if (alarm) { - var val, offset = $('#taskedit select.edit-alarm-offset').val(); - if (offset == '@') - me.selected_task.alarms = '@' + date2unixtime(parse_datetime($('#taskedit input.edit-alarm-time').val(), $('#taskedit input.edit-alarm-date').val())) + ':' + alarm; - else if ((val = parseInt($('#taskedit input.edit-alarm-value').val())) && !isNaN(val) && val >= 0) - me.selected_task.alarms = offset[0] + val + offset[1] + ':' + alarm; - } - // uploaded attachments list for (var i in rcmail.env.attachments) { if (i.match(/^rcmfile(.+)/)) diff --git a/plugins/tasklist/tasklist.php b/plugins/tasklist/tasklist.php index d53b0a8c..aae960ab 100644 --- a/plugins/tasklist/tasklist.php +++ b/plugins/tasklist/tasklist.php @@ -406,9 +406,16 @@ class tasklist extends rcube_plugin $rec['tags'] = array_filter((array)$rec['tags']); } - // alarms cannot work without a date - if ($rec['alarms'] && !$rec['date'] && !$rec['startdate'] && strpos($rec['alarms'], '@') === false) - $rec['alarms'] = ''; + // convert the submitted alarm values + if ($rec['valarms']) { + $valarms = array(); + foreach (libcalendaring::from_client_alarms($rec['valarms']) as $alarm) { + // alarms can only work with a date (either task start, due or absolute alarm date) + if (is_a($alarm['trigger'], 'DateTime') || $rec['date'] || $rec['startdate']) + $valarms[] = $alarm; + } + $rec['valarms'] = $valarms; + } $attachments = array(); $taskid = $rec['id']; @@ -663,8 +670,10 @@ class tasklist extends rcube_plugin } } - if ($rec['alarms']) - $rec['alarms_text'] = libcalendaring::alarms_text($rec['alarms']); + if ($rec['valarms']) { + $rec['alarms_text'] = libcalendaring::alarms_text($rec['valarms']); + $rec['valarms'] = libcalendaring::to_client_alarms($rec['valarms']); + } foreach ((array)$rec['attachments'] as $k => $attachment) { $rec['attachments'][$k]['classname'] = rcube_utils::file2class($attachment['mimetype'], $attachment['name']);