diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php index df6e418f..91975a3d 100644 --- a/plugins/calendar/calendar.php +++ b/plugins/calendar/calendar.php @@ -108,6 +108,8 @@ class calendar extends rcube_plugin $this->register_action('export_events', array($this, 'export_events')); $this->register_action('upload', array($this, 'attachment_upload')); $this->register_action('get-attachment', array($this, 'attachment_get')); + $this->register_action('freebusy-status', array($this, 'freebusy_status')); + $this->register_action('freebusy-times', array($this, 'freebusy_times')); $this->register_action('randomdata', array($this, 'generate_randomdata')); } else if ($this->rc->task == 'settings') { @@ -169,10 +171,12 @@ class calendar extends rcube_plugin $this->register_handler('plugin.recurrence_form', array($this->ui, 'recurrence_form')); $this->register_handler('plugin.attachments_form', array($this->ui, 'attachments_form')); $this->register_handler('plugin.attachments_list', array($this->ui, 'attachments_list')); + $this->register_handler('plugin.attendees_list', array($this->ui, 'attendees_list')); + $this->register_handler('plugin.attendees_form', array($this->ui, 'attendees_form')); $this->register_handler('plugin.edit_recurring_warning', array($this->ui, 'recurring_event_warning')); $this->register_handler('plugin.searchform', array($this->rc->output, 'search_form')); // use generic method from rcube_template - $this->rc->output->add_label('low','normal','high','delete','cancel','uploading'); + $this->rc->output->add_label('low','normal','high','delete','cancel','uploading','noemailwarning'); $this->rc->output->send("calendar.calendar"); } @@ -442,29 +446,41 @@ class calendar extends rcube_plugin case "new": // create UID for new event $event['uid'] = $this->generate_uid(); + + // set current user as organizer + if (!$event['attendees']) { + $identity = $this->rc->user->get_identity(); + $event['attendees'][] = array('role' => 'OWNER', 'name' => $identity['name'], 'email' => $identity['email']); + } + $this->prepare_event($event); if ($success = $this->driver->new_event($event)) $this->cleanup_event($event); $reload = true; break; + case "edit": $this->prepare_event($event); if ($success = $this->driver->edit_event($event)) $this->cleanup_event($event); $reload = true; break; + case "resize": $success = $this->driver->resize_event($event); $reload = true; break; + case "move": $success = $this->driver->move_event($event); $reload = true; break; + case "remove": $removed = $this->driver->remove_event($event); $reload = true; break; + case "dismiss": foreach (explode(',', $event['id']) as $id) $success |= $this->driver->dismiss_alarm($id, $event['snooze']); @@ -606,6 +622,10 @@ class calendar extends rcube_plugin // user prefs $settings['hidden_calendars'] = array_filter(explode(',', $this->rc->config->get('hidden_calendars', ''))); + + // get user identity to create default attendee + $identity = $this->rc->user->get_identity(); + $settings['event_owner'] = array('name' => $identity['name'], 'email' => $identity['email']); return $settings; } @@ -1113,5 +1133,59 @@ class calendar extends rcube_plugin unset($_SESSION['event_session']); } } + + /** + * Echo simple free/busy status text for the given user and time range + */ + public function freebusy_status() + { + $email = get_input_value('email', RCUBE_INPUT_GPC); + $start = get_input_value('start', RCUBE_INPUT_GET); + $end = get_input_value('end', RCUBE_INPUT_GET); + + if (!$start) $start = time(); + if (!$end) $end = $start + 3600; + + $status = 'UNKNOWN'; + + // if the backend has free-busy information + $fblist = $this->driver->get_freebusy_list($email, $start, $end); + if (is_array($fblist)) { + $status = 'FREE'; + + foreach ($fblist as $slot) { + list($from, $to) = $slot; + if ($from <= $end && $to >= $start) { + $status = 'BUSY'; + break; + } + } + } + + echo $status; + exit; + } + + /** + * Return a list of free/busy time slots within the given period + * Echo data in JSON encoding + */ + public function freebusy_times() + { + $email = get_input_value('email', RCUBE_INPUT_GPC); + $start = get_input_value('start', RCUBE_INPUT_GET); + $end = get_input_value('end', RCUBE_INPUT_GET); + + if (!$start) $start = time(); + if (!$end) $end = $start + 86400 * 30; + + $fblist = $this->driver->get_freebusy_list($email, $start, $end); + $result = array(); + + // TODO: build a list from $start till $end with blocks representing the fb-status + + echo json_encode($result); + exit; + } } diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js index e71467e7..fe0925ca 100644 --- a/plugins/calendar/calendar_ui.js +++ b/plugins/calendar/calendar_ui.js @@ -42,6 +42,8 @@ function rcube_calendar_ui(settings) var gmt_offset = (new Date().getTimezoneOffset() / -60) - (settings.timezone || 0); var day_clicked = day_clicked_ts = 0; var ignore_click = false; + var event_attendees = null; + var attendees_list; // general datepicker settings var datepicker_settings = { @@ -66,6 +68,27 @@ function rcube_calendar_ui(settings) { return String(str).replace(/\n/g, "
"); }; + + // + var explode_quoted_string = function(str, delimiter) + { + var result = [], + strlen = str.length, + q, p, i; + + for (q = p = i = 0; i < strlen; i++) { + if (str[i] == '"' && str[i-1] != '\\') { + q = !q; + } + else if (!q && str[i] == delimiter) { + result.push(str.substring(p, i)); + p = i + 1; + } + } + + result.push(str.substr(p)); + return result; + }; // from time and date strings to a real date object var parse_datetime = function(time, date) @@ -390,6 +413,14 @@ function rcube_calendar_ui(settings) } else $('#edit-recurring-warning').hide(); + + // attendees + event_attendees = []; + attendees_list = $('#edit-attendees-table > tbody').html(''); + if (calendar.attendees && event.attendees) { + for (var j=0; j < event.attendees.length; j++) + add_attendee(event.attendees[j]); + } // attachments if (calendar.attachments) { @@ -436,6 +467,7 @@ function rcube_calendar_ui(settings) sensitivity: sensitivity.val(), recurrence: '', alarms: '', + attendees: event_attendees, deleted_attachments: rcmail.env.deleted_attachments }; @@ -531,18 +563,87 @@ function rcube_calendar_ui(settings) $dialog.dialog({ modal: true, resizable: true, + closeOnEscape: false, title: rcmail.gettext((action == 'edit' ? 'edit_event' : 'new_event'), 'calendar'), close: function() { $dialog.dialog("destroy").hide(); }, buttons: buttons, - minWidth: 440, - width: 480 + minWidth: 500, + width: 580 }).show(); title.select(); }; + // add the given list of participants + var add_attendees = function(names) + { + names = explode_quoted_string(names.replace(/,\s*$/, ''), ','); + + // parse name/email pairs + var item, email, name, success = false; + for (var i=0; i < names.length; i++) { + email = name = null; + item = $.trim(names[i]); + + if (!item.length) { + continue; + } // address in brackets without name (do nothing) + else if (item.match(/^<[^@]+@[^>]+>$/)) { + email = item.replace(/[<>]/g, ''); + } // address without brackets and without name (add brackets) + else if (rcube_check_email(item)) { + email = item; + } // address with name + else if (item.match(/([^\s<@]+@[^>]+)>*$/)) { + email = RegExp.$1; + name = item.replace(email, '').replace(/^["\s<>]+/, '').replace(/["\s<>]+$/, ''); + } + + if (email) { + add_attendee({ email:email, name:name, role:'REQUIRED', status:'unknown' }); + success = true; + } + else { + alert(rcmail.gettext('noemailwarning')); + } + } + + return success; + }; + + // add the given attendee to the list + var add_attendee = function(data) + { + var dispname = (data.email && data.name) ? data.name + ' <' + data.email + '>' : (data.email || data.name); + + // delete icon + var icon = rcmail.env.deleteicon ? '' : rcmail.gettext('delete'); + var dellink = '' + icon + ''; + + var html = '' + + '' + Q(dispname) + '' + + '' + '' + '' + + '' + Q(data.status) + '' + + '' + (data.role != 'OWNER' ? dellink : '') + ''; + + $('') + .addClass(String(data.role).toLowerCase()) + .html(html) + .appendTo(attendees_list) + .find('a.deletelink').click({ id:(data.email || data.name) }, function(e) { remove_attendee(this, e.data.id); return false; }); + + event_attendees.push(data); + }; + + // remove an attendee from the list + var remove_attendee = function(elem, id) + { + $(elem).closest('tr').hide(); + event_attendees = $.grep(event_attendees, function(data){ return (data.name != id && data.email != id) }); + }; + // post the given event data to server var update_event = function(action, data) { @@ -1098,7 +1199,12 @@ function rcube_calendar_ui(settings) }; // init event dialog - $('#eventtabs').tabs(); + $('#eventtabs').tabs({ + show: function(event, ui) { + if (ui.panel.id == 'event-tab-3') + $('#edit-attendee-name').select(); + } + }); $('#edit-enddate, input.edit-alarm-date').datepicker(datepicker_settings); $('#edit-startdate').datepicker(datepicker_settings).datepicker('option', 'onSelect', shift_enddate).change(function(){ shift_enddate(this.value); }); $('#edit-allday').click(function(){ $('#edit-starttime, #edit-endtime')[(this.checked?'hide':'show')](); }); @@ -1175,6 +1281,16 @@ function rcube_calendar_ui(settings) $('#recurrence-form-'+freq+', #recurrence-form-until').show(); }); $('#edit-recurrence-enddate').datepicker(datepicker_settings).click(function(){ $("#edit-recurrence-repeat-until").prop('checked', true) }); + + // init attendees autocompletion + rcmail.init_address_input_events($('#edit-attendee-name')); + rcmail.addEventListener('autocomplete_insert', function(e){ $('#edit-attendee-add').click(); }); + + $('#edit-attendee-add').click(function(){ + var input = $('#edit-attendee-name'); + if (add_attendees(input.val())) + input.val(''); + }); // add proprietary css styles if not IE if (!bw.ie) @@ -1208,7 +1324,6 @@ window.rcmail && rcmail.addEventListener('init', function(evt) { rcmail.addEventListener('plugin.destroy_source', function(p){ cal.calendar_destroy_source(p.id); }); rcmail.addEventListener('plugin.unlock_saving', function(p){ rcmail.set_busy(false, null, cal.saving_lock); }); - // let's go var cal = new rcube_calendar_ui(rcmail.env.calendar_settings); diff --git a/plugins/calendar/drivers/calendar_driver.php b/plugins/calendar/drivers/calendar_driver.php index 2aa48693..01c68d52 100644 --- a/plugins/calendar/drivers/calendar_driver.php +++ b/plugins/calendar/drivers/calendar_driver.php @@ -274,10 +274,15 @@ abstract class calendar_driver /** * Fetch free/busy information from a person within the given range + * + * @param string E-mail address of attendee + * @param integer Requested period start date/time as unix timestamp + * @param integer Requested period end date/time as unix timestamp + * @return array List of busy timeslots within the requested range */ public function get_freebusy_list($email, $start, $end) { - return array(); + return false; } /** diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php index 3c0f6151..d7ebd10a 100644 --- a/plugins/calendar/drivers/kolab/kolab_calendar.php +++ b/plugins/calendar/drivers/kolab/kolab_calendar.php @@ -441,6 +441,24 @@ class kolab_calendar ); } } + + if ($rec['organizer']) { + $attendees[] = array( + 'role' => 'OWNER', + 'name' => $rec['organizer']['display-name'], + 'email' => $rec['organizer']['smtp-address'], + 'status' => 'accepted', + ); + } + + foreach ((array)$rec['attendee'] as $attendee) { + $attendees[] = array( + 'role' => strtoupper($attendee['role']), + 'name' => $attendee['display-name'], + 'email' => $attendee['smtp-address'], + 'status' => $attendee['status'], + ); + } return array( 'id' => $rec['uid'], @@ -455,6 +473,7 @@ class kolab_calendar 'alarms' => $alarm_value . $alarm_unit, 'categories' => $rec['categories'], 'attachments' => $attachments, + 'attendees' => $attendees, 'free_busy' => $rec['show-time-as'], 'priority' => isset($priority_map[$rec['priority']]) ? $priority_map[$rec['priority']] : 1, 'sensitivity' => $sensitivity_map[$rec['sensitivity']], @@ -601,6 +620,25 @@ class kolab_calendar unset($event['attachments'][$idx]); } } + + // process event attendees + foreach ((array)$event['attendees'] as $attendee) { + $role = $attendee['role']; + if ($role == 'OWNER') { + $object['organizer'] = array( + 'display-name' => $attendee['name'], + 'smtp-address' => $attendee['email'], + ); + } + else { + $object['attendee'][] = array( + 'display-name' => $attendee['name'], + 'smtp-address' => $attendee['email'], + 'status' => $attendee['status'], + 'role' => strtolower($role), + ); + } + } return $object; } diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php index bda22f3c..18c9b237 100644 --- a/plugins/calendar/drivers/kolab/kolab_driver.php +++ b/plugins/calendar/drivers/kolab/kolab_driver.php @@ -645,7 +645,29 @@ class kolab_driver extends calendar_driver */ public function get_freebusy_list($email, $start, $end) { - return array(); + require_once('Horde/iCalendar.php'); + + if (empty($email) || $end < time()) + return false; + + // load and parse free-busy information using Horde classes + $fburl = rcube_kolab::get_freebusy_url($email); + if ($fbdata = file_get_contents($fburl)) { + $fbcal = new Horde_iCalendar; + $fbcal->parsevCalendar($fbdata); + if ($fb = $fbcal->findComponent('vfreebusy')) { + $result = array(); + foreach ($fb->getBusyPeriods() as $from => $to) { + if ($to == null) // no information, assume free + break; + $result[] = array($from, $to); + } + + return $result; + } + } + + return false; } } diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php index f21faa45..6fefdf7e 100644 --- a/plugins/calendar/lib/calendar_ui.php +++ b/plugins/calendar/lib/calendar_ui.php @@ -560,5 +560,31 @@ class calendar_ui return html::tag('form', array('action' => "#", 'method' => "get"), $html); } + + /** + * + */ + function attendees_list($attrib = array()) + { + $table = new html_table(array('cols' => 5, 'border' => 0, 'cellpadding' => 0, 'class' => 'rectable')); + $table->add_header('role', $this->calendar->gettext('role')); + $table->add_header('name', $this->calendar->gettext('attendee')); + $table->add_header('availability', $this->calendar->gettext('availability')); + $table->add_header('confirmstate', $this->calendar->gettext('confirmstate')); + $table->add_header('options', ''); + + return $table->show($attrib); + } + + /** + * + */ + function attendees_form($attrib = array()) + { + $input = new html_inputfield(array('name' => 'participant', 'id' => 'edit-attendee-name', 'size' => 30)); + + return html::div($attrib, $input->show() . " " . + html::tag('input', array('type' => 'button', 'class' => 'button', 'id' => 'edit-attendee-add', 'value' => $this->calendar->gettext('addattendee')))); + } } diff --git a/plugins/calendar/localization/en_US.inc b/plugins/calendar/localization/en_US.inc index 2f476a82..43183ee3 100644 --- a/plugins/calendar/localization/en_US.inc +++ b/plugins/calendar/localization/en_US.inc @@ -86,6 +86,13 @@ $labels['repeattomorrow'] = 'Repeat tomorrow'; $labels['repeatinweek'] = 'Repeat in a week'; $labels['alarmtitle'] = 'Upcoming events'; +// attendees +$labels['attendee'] = 'Participant'; +$labels['role'] = 'Role'; +$labels['availability'] = 'Avail.'; +$labels['confirmstate'] = 'Confirmed'; +$labels['addattendee'] = 'Add participant'; + // event dialog tabs $labels['tabsummary'] = 'Summary'; $labels['tabrecurrence'] = 'Recurrence'; diff --git a/plugins/calendar/skins/default/calendar.css b/plugins/calendar/skins/default/calendar.css index e8853774..d9aebb71 100644 --- a/plugins/calendar/skins/default/calendar.css +++ b/plugins/calendar/skins/default/calendar.css @@ -467,6 +467,53 @@ td.topalign { min-width: 5em; } +#edit-attendees-table { + width: 100%; + display: table; + table-layout: fixed; + border-collapse: collapse; + border: 1px solid #ddd; +} + +#edit-attendees-table td { + padding: 3px; + border-bottom: 1px solid #ddd; +} + +#edit-attendees-table tr.owner td { + color: #999; +} + +#edit-attendees-table td.role, +#edit-attendees-table td.availability { + width: 4em; +} + +#edit-attendees-table td.confirmstate { + width: 6em; +} + +#edit-attendees-table td.options { + width: 3em; + text-align: right; + padding-right: 4px; +} + +#edit-attendees-table td.name { + width: auto; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +#edit-attendees-table thead td { + background: url(images/listheader.gif) top left repeat-x #CCC; +} + +#edit-attendees-form { + margin-top: 1em; +} + span.edit-alarm-set { white-space: nowrap; } @@ -623,7 +670,8 @@ div.fc-event-location { } .fc-view-list div.fc-list-header, -.fc-view-table td.fc-list-header { +.fc-view-table td.fc-list-header, +#edit-attendees-table thead td { padding: 3px; background: #dddddd; background-image: -moz-linear-gradient(center top, #f4f4f4, #d2d2d2); diff --git a/plugins/calendar/skins/default/templates/calendar.html b/plugins/calendar/skins/default/templates/calendar.html index 2a75662f..2c8d68bc 100644 --- a/plugins/calendar/skins/default/templates/calendar.html +++ b/plugins/calendar/skins/default/templates/calendar.html @@ -166,7 +166,8 @@
- + +
diff --git a/plugins/kolab_core/rcube_kolab.php b/plugins/kolab_core/rcube_kolab.php index be231a3f..59841d2d 100644 --- a/plugins/kolab_core/rcube_kolab.php +++ b/plugins/kolab_core/rcube_kolab.php @@ -27,6 +27,7 @@ require_once 'Horde/Perms.php'; class rcube_kolab { private static $horde_auth; + private static $config; private static $ready = false; @@ -74,6 +75,7 @@ class rcube_kolab $conf['kolab']['ldap'] = array_merge($ldap, (array)$conf['kolab']['ldap']); $conf['kolab']['imap'] = array_merge($imap, (array)$conf['kolab']['imap']); + self::$config = &$conf; // pass the current IMAP authentication credentials to the Horde auth system self::$horde_auth = Auth::singleton('kolab'); @@ -146,6 +148,15 @@ class rcube_kolab $kolab = Kolab_List::singleton(); return self::$ready ? $kolab->getFolder($folder)->getData($data_type) : null; } + + /** + * Compose an URL to query the free/busy status for the given user + */ + public static function get_freebusy_url($email) + { + $host = self::$config['kolab']['freebusy']['server'] ? self::$config['kolab']['freebusy']['server'] : self::$config['kolab']['imap']['server']; + return 'https://' . $host . '/freebusy/' . $email . '.ifb'; + } /** * Cleanup session data when done