diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php index 0623d08d..1c5801a1 100644 --- a/plugins/calendar/calendar.php +++ b/plugins/calendar/calendar.php @@ -29,6 +29,7 @@ class calendar extends rcube_plugin const FREEBUSY_UNKNOWN = 0; const FREEBUSY_FREE = 1; const FREEBUSY_BUSY = 2; + const FREEBUSY_TENTATIVE = 3; const FREEBUSY_OOF = 4; public $task = '?(?!login|logout).*'; @@ -51,6 +52,8 @@ class calendar extends rcube_plugin 'calendar_timeslots' => 2, 'calendar_first_day' => 1, 'calendar_first_hour' => 6, + 'calendar_work_start' => 6, + 'calendar_work_end' => 18, ); private $default_categories = array( @@ -116,7 +119,7 @@ class calendar extends rcube_plugin $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')); - $this->register_action('print',array($this,'print_view')); + $this->register_action('print', array($this,'print_view')); // remove undo information... if ($undo = $_SESSION['calendar_event_undo']) { @@ -508,6 +511,7 @@ class calendar extends rcube_plugin . ' ' . html::a(array('onclick' => sprintf("%s.http_request('event', 'action=undo', %s.display_message('', 'loading'))", JS_OBJECT_NAME, JS_OBJECT_NAME)), rcube_label('undo')); $this->rc->output->show_message($msg, 'confirmation', null, true, $undo_time); + $got_msg = true; } else if ($success) { $this->rc->output->show_message('calendar.successremoval', 'confirmation'); @@ -597,10 +601,11 @@ class calendar extends rcube_plugin $end = get_input_value('end', RCUBE_INPUT_GET); if (!$start) $start = mktime(0, 0, 0, 1, date('n'), date('Y')-1); if (!$end) $end = mktime(0, 0, 0, 31, 12, date('Y')+10); - $events = $this->driver->load_events($start, $end, null, get_input_value('source', RCUBE_INPUT_GET), 0); + $calendar_name = get_input_value('source', RCUBE_INPUT_GET); + $events = $this->driver->load_events($start, $end, null, $calendar_name, 0); header("Content-Type: text/calendar"); - header("Content-Disposition: inline; filename=calendar.ics"); + header("Content-Disposition: inline; filename=".$calendar_name); echo $this->ical->export($events); exit; @@ -624,6 +629,8 @@ class calendar extends rcube_plugin $settings['timeslots'] = (int)$this->rc->config->get('calendar_timeslots', $this->defaults['calendar_timeslots']); $settings['first_day'] = (int)$this->rc->config->get('calendar_first_day', $this->defaults['calendar_first_day']); $settings['first_hour'] = (int)$this->rc->config->get('calendar_first_hour', $this->defaults['calendar_first_hour']); + $settings['work_start'] = (int)$this->rc->config->get('calendar_work_start', $this->defaults['calendar_work_start']); + $settings['work_end'] = (int)$this->rc->config->get('calendar_work_end', $this->defaults['calendar_work_end']); $settings['timezone'] = $this->timezone; // localization @@ -1204,6 +1211,7 @@ class calendar extends rcube_plugin if (!$start) $start = time(); if (!$end) $end = $start + 3600; + $fbtypemap = array(calendar::FREEBUSY_FREE => 'FREE', calendar::FREEBUSY_BUSY => 'BUSY', calendar::FREEBUSY_TENTATIVE => 'TENTATIVE', calendar::FREEBUSY_OOF => 'OUT-OF-OFFICE'); $status = 'UNKNOWN'; // if the backend has free-busy information @@ -1212,9 +1220,9 @@ class calendar extends rcube_plugin $status = 'FREE'; foreach ($fblist as $slot) { - list($from, $to) = $slot; + list($from, $to, $type) = $slot; if ($from <= $end && $to > $start) { - $status = 'BUSY'; + $status = $type && $fbtypemap[$type] ? $fbtypemap[$type] : 'BUSY'; break; } } @@ -1254,9 +1262,9 @@ class calendar extends rcube_plugin if (is_array($fblist)) { $status = self::FREEBUSY_FREE; foreach ($fblist as $slot) { - list($from, $to) = $slot; + list($from, $to, $type) = $slot; if ($from <= $t_end && $to > $t) { - $status = self::FREEBUSY_BUSY; + $status = isset($type) ? $type : self::FREEBUSY_BUSY; break; } } diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js index 6530a012..bfa2614d 100644 --- a/plugins/calendar/calendar_ui.js +++ b/plugins/calendar/calendar_ui.js @@ -42,10 +42,12 @@ 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_defaults = { free_busy:'busy' }; var event_attendees = null; var attendees_list; - var freebusy_ui = {}; + var freebusy_ui = { workinhoursonly:false }; var freebusy_data = {}; + var freebusy_needsupdate; // general datepicker settings var datepicker_settings = { @@ -329,7 +331,9 @@ function rcube_calendar_ui(settings) var $dialog = $("#eventedit"); var calendar = event.calendar && me.calendars[event.calendar] ? me.calendars[event.calendar] : { editable:action=='new' }; - me.selected_event = event; + me.selected_event = $.extend(event_defaults, event); // clone event object (with defaults) + event = me.selected_event; // change reference to clone + freebusy_needsupdate = false; // reset dialog first, enable/disable fields according to editable state $('#eventtabs').get(0).reset(); @@ -614,27 +618,34 @@ function rcube_calendar_ui(settings) return false; // set form elements - var duration = Math.round((event.end.getTime() - event.start.getTime()) / 1000); - var startdate = $('#schedule-startdate').val($.fullCalendar.formatDate(event.start, settings['date_format'])).data('duration', duration); - var starttime = $('#schedule-starttime').val($.fullCalendar.formatDate(event.start, settings['time_format'])).show(); - var enddate = $('#schedule-enddate').val($.fullCalendar.formatDate(event.end, settings['date_format'])); - var endtime = $('#schedule-endtime').val($.fullCalendar.formatDate(event.end, settings['time_format'])).show(); var allday = $('#edit-allday').get(0); + var duration = Math.round((event.end.getTime() - event.start.getTime()) / 1000); + freebusy_ui.startdate = $('#schedule-startdate').val($.fullCalendar.formatDate(event.start, settings['date_format'])).data('duration', duration); + freebusy_ui.starttime = $('#schedule-starttime').val($.fullCalendar.formatDate(event.start, settings['time_format'])).show(); + freebusy_ui.enddate = $('#schedule-enddate').val($.fullCalendar.formatDate(event.end, settings['date_format'])); + freebusy_ui.endtime = $('#schedule-endtime').val($.fullCalendar.formatDate(event.end, settings['time_format'])).show(); if (allday.checked) { starttime.val("00:00").hide(); endtime.val("23:59").hide(); + event.allDay = true; } + // read attendee roles from drop-downs + $('select.edit-attendee-role').each(function(i, elem){ + if (event_attendees[i]) + event_attendees[i].role = $(elem).val(); + }); + // render time slots var now = new Date(), fb_start = new Date(), fb_end = new Date(); - fb_start.setTime(Math.max(now, event.start)); + fb_start.setTime(event.start); fb_start.setHours(0); fb_start.setMinutes(0); fb_start.setSeconds(0); fb_start.setMilliseconds(0); fb_end.setTime(fb_start.getTime() + 86400000); freebusy_data = {}; - freebusy_ui.loading = 1; // prevent render_freebusy_grid() to load data - freebusy_ui.numdays = allday.checked ? 7 : 1; + freebusy_ui.loading = 1; // prevent render_freebusy_grid() to load data yet + freebusy_ui.numdays = allday.checked ? 7 : Math.ceil(duration * 2 / 86400); freebusy_ui.interval = allday.checked ? 360 : 60; freebusy_ui.start = fb_start; freebusy_ui.end = new Date(freebusy_ui.start.getTime() + 86400000 * freebusy_ui.numdays); @@ -651,9 +662,23 @@ function rcube_calendar_ui(settings) $('#schedule-attendees-list').html(list_html); + // enable/disable buttons + $('#shedule-find-prev').button('option', 'disabled', (fb_start.getTime() < now.getTime())); + // dialog buttons var buttons = {}; + buttons[rcmail.gettext('adobt', 'calendar')] = function() { + $('#edit-startdate').val(freebusy_ui.startdate.val()); + $('#edit-starttime').val(freebusy_ui.starttime.val()); + $('#edit-enddate').val(freebusy_ui.enddate.val()); + $('#edit-endtime').val(freebusy_ui.endtime.val()); + if (freebusy_needsupdate) + update_freebusy_status(me.selected_event); + freebusy_needsupdate = false; + $dialog.dialog("close"); + }; + buttons[rcmail.gettext('cancel', 'calendar')] = function() { $dialog.dialog("close"); }; @@ -664,6 +689,8 @@ function rcube_calendar_ui(settings) closeOnEscape: true, title: rcmail.gettext('scheduletime', 'calendar'), close: function() { + if (bw.ie6) + $("#edit-attendees-table").css('visibility','visible'); $dialog.dialog("destroy").hide(); }, buttons: buttons, @@ -671,6 +698,18 @@ function rcube_calendar_ui(settings) width: 850 }).show(); + // hide edit dialog on IE6 because of drop-down elements + if (bw.ie6) + $("#edit-attendees-table").css('visibility','hidden'); + + // adjust dialog size to fit grid without scrolling + var gridw = $('#schedule-freebusy-times').width(); + var overflow = gridw - $('#attendees-freebusy-table td.times').width() + 1; + if (overflow > 0) { + $dialog.dialog('option', 'width', Math.min((window.innerWidth || document.documentElement.clientWidth) - 40, 850 + overflow)); + $dialog.dialog('option', 'position', ['center', 'center']); + } + // fetch data from server freebusy_ui.loading = 0; load_freebusy_data(freebusy_ui.start, freebusy_ui.interval); @@ -694,8 +733,8 @@ function rcube_calendar_ui(settings) lastdate = datestr; } - // TODO: define working hours by config - css = (freebusy_ui.numdays == 1 && (curdate.getHours() < 6 || curdate.getHours() > 18)) ? 'offhours' : 'workinghours'; + // set css class according to working hours + css = (freebusy_ui.numdays == 1 && (curdate.getHours() < settings['work_start'] || curdate.getHours() > settings['work_end'])) ? 'offhours' : 'workinghours'; times_row += '' + Q($.fullCalendar.formatDate(curdate, settings['time_format'])) + ''; slots_row += ' '; @@ -712,23 +751,98 @@ function rcube_calendar_ui(settings) times_html += '' + slots_row + ''; } - $('#schedule-freebusy-times > thead').html(dates_row + times_row); - $('#schedule-freebusy-times > tbody').html(times_html); + var table = $('#schedule-freebusy-times'); + table.children('thead').html(dates_row + times_row); + table.children('tbody').html(times_html); // if we have loaded free-busy data, show it if (!freebusy_ui.loading) { if (date2unixtime(freebusy_ui.start) < freebusy_data.start || date2unixtime(freebusy_ui.end) > freebusy_data.end || freebusy_ui.interval != freebusy_data.interval) { - load_freebusy_data(freebusy_ui.start, freebusy_ui.interval) - return; + load_freebusy_data(freebusy_ui.start, freebusy_ui.interval); } - - for (var email, i=0; i < event_attendees.length; i++) { - if ((email = event_attendees[i].email)) - update_freebusy_display(email); + else { + for (var email, i=0; i < event_attendees.length; i++) { + if ((email = event_attendees[i].email)) + update_freebusy_display(email); + } } } + + // render current event date/time selection over grid table + // use timeout to let the dom attributes (width/height/offset) be set first + window.setTimeout(function(){ render_freebusy_overlay(); }, 10); }; + // render overlay element over the grid to visiualize the current event date/time + var render_freebusy_overlay = function() + { + var overlay = $('#schedule-event-time'); + if (me.selected_event.end.getTime() < freebusy_ui.start.getTime() || me.selected_event.start.getTime() > freebusy_ui.end.getTime()) { + overlay.draggable('disable').hide(); + } + else { + var table = $('#schedule-freebusy-times'), + width = 0, + pos = { top:table.children('thead').height(), left:0 }, + eventstart = date2unixtime(me.selected_event.start), + eventend = date2unixtime(me.selected_event.end), + slotstart = date2unixtime(freebusy_ui.start), + slotsize = freebusy_ui.interval * 60, + slotend, fraction, $cell; + + // iterate through slots to determine position and size of the overlay + table.children('thead').find('td').each(function(i, cell){ + slotend = slotstart + slotsize - 60; + // event starts in this slot: compute left + if (eventstart >= slotstart && eventstart <= slotend) { + fraction = 1 - (slotend - eventstart) / slotsize; + pos.left = Math.round(cell.offsetLeft + cell.offsetWidth * fraction); + } + // event ends in this slot: compute width + else if (eventend >= slotstart && eventend <= slotend) { + fraction = 1 - (slotend - eventend) / slotsize; + width = Math.round(cell.offsetLeft + cell.offsetWidth * fraction) - pos.left; + } + + slotstart = slotstart + slotsize; + }); + + if (!width) + width = table.width() - pos.left; + + // overlay is visible + if (width > 0) { + overlay.css({ width: (width-5)+'px', height:(table.children('tbody').height() - 4)+'px', left:pos.left+'px', top:pos.top+'px' }).draggable('enable').show(); + + // configure draggable + if (!overlay.data('isdraggable')) { + overlay.draggable({ + axis: 'x', + scroll: true, + stop: function(e, ui){ + // convert pixels to time + var px = ui.position.left; + var range_p = $('#schedule-freebusy-times').width(); + var range_t = freebusy_ui.end.getTime() - freebusy_ui.start.getTime(); + var newstart = new Date(freebusy_ui.start.getTime() + px * (range_t / range_p)); + newstart.setSeconds(0); newstart.setMilliseconds(0); + // round to 5 minutes + var round = newstart.getMinutes() % 5; + if (round > 2.5) newstart.setTime(newstart.getTime() + (5 - round) * 60000); + else if (round > 0) newstart.setTime(newstart.getTime() - round * 60000); + // update event times + update_freebusy_dates(newstart, new Date(newstart.getTime() + freebusy_ui.startdate.data('duration') * 1000)); + } + }).data('isdraggable', true); + } + } + else + overlay.draggable('disable').hide(); + } + + }; + + // fetch free-busy information for each attendee from server var load_freebusy_data = function(from, interval) { @@ -790,6 +904,109 @@ function rcube_calendar_ui(settings) }); } }; + + // write changed event date/times back to form fields + var update_freebusy_dates = function(start, end) + { + me.selected_event.start = start; + me.selected_event.end = end; + freebusy_ui.startdate.val($.fullCalendar.formatDate(start, settings['date_format'])); + freebusy_ui.starttime.val($.fullCalendar.formatDate(start, settings['time_format'])); + freebusy_ui.enddate.val($.fullCalendar.formatDate(end, settings['date_format'])); + freebusy_ui.endtime.val($.fullCalendar.formatDate(end, settings['time_format'])); + freebusy_needsupdate = true; + }; + + // attempt to find a time slot where all attemdees are available + var freebusy_find_slot = function(dir) + { + var event = me.selected_event, + eventstart = date2unixtime(event.start), // calculate with unitimes + eventend = date2unixtime(event.end), + duration = eventend - eventstart, + sinterval = freebusy_data.interval * 60, + intvlslots = event.allDay ? 4 : 1, + numslots = Math.ceil(duration / sinterval), + checkdate, slotend, email, curdate; + + // shift event times to next possible slot + eventstart += sinterval * intvlslots * dir; + eventend += sinterval * intvlslots * dir; + + // iterate through free-busy slots and find candidates + var candidatecount = 0, candidatestart = candidateend = success = false; + for (var slot = dir > 0 ? freebusy_data.start : freebusy_data.end - sinterval; (dir > 0 && slot < freebusy_data.end) || (dir < 0 && slot >= freebusy_data.start); slot += sinterval * dir) { + slotend = slot + sinterval; + if ((dir > 0 && slotend <= eventstart) || (dir < 0 && slot >= eventend)) // skip + continue; + + // respect workingours setting + if (freebusy_ui.workinhoursonly && freebusy_data.interval <= 60) { + curdate = fromunixtime(dir > 0 || !candidateend ? slot : (candidateend - duration)); + if (curdate.getHours() < settings['work_start'] || curdate.getHours() > settings['work_end']) { // skip off-hours + candidatestart = candidateend = false; + candidatecount = 0; + continue; + } + } + + if (!candidatestart) + candidatestart = slot; + + // check freebusy data for all attendees + for (var i=0; i < event_attendees.length; i++) { + if ((email = event_attendees[i].email) && freebusy_data[email][slot] > 1) { + candidatestart = candidateend = false; + break; + } + } + + // occupied slot + if (!candidatestart) { + slot += Math.max(0, intvlslots - candidatecount - 1) * sinterval * dir; + candidatecount = 0; + continue; + } + + // set candidate end to slot end time + candidatecount++; + if (dir < 0 && !candidateend) + candidateend = slotend; + + // if candidate is big enough, this is it! + if (candidatecount == numslots) { + if (dir > 0) { + event.start = fromunixtime(candidatestart); + event.end = fromunixtime(candidatestart + duration); + } + else { + event.end = fromunixtime(candidateend); + event.start = fromunixtime(candidateend - duration); + } + success = true; + break; + } + } + + // update event date/time display + if (success) { + update_freebusy_dates(event.start, event.end); + + // move freebusy grid if necessary + if (event.start.getTime() >= freebusy_ui.end.getTime()) + render_freebusy_grid(1); + else if (event.end.getTime() <= freebusy_ui.start.getTime()) + render_freebusy_grid(-1); + else + render_freebusy_overlay(); + + var now = new Date(); + $('#shedule-find-prev').button('option', 'disabled', (event.start.getTime() < now.getTime())); + } + else { + alert(rcmail.gettext('noslotfound','calendar')); + } + }; // update event properties and attendees availability if event times have changed @@ -797,10 +1014,11 @@ function rcube_calendar_ui(settings) { if (me.selected_event) { var allday = $('#edit-allday').get(0); + me.selected_event.allDay = allday.checked; me.selected_event.start = parse_datetime(allday.checked ? '00:00' : $('#edit-starttime').val(), $('#edit-startdate').val()); me.selected_event.end = parse_datetime(allday.checked ? '23:59' : $('#edit-endtime').val(), $('#edit-enddate').val()); - if (me.selected_event.attendees) - update_freebusy_status(me.selected_event); + if (event_attendees) + freebusy_needsupdate = true; $('#edit-startdate').data('duration', Math.round((me.selected_event.end.getTime() - me.selected_event.start.getTime()) / 1000)); } }; @@ -902,10 +1120,12 @@ function rcube_calendar_ui(settings) var update_freebusy_status = function(event) { var icons = attendees_list.find('img.availabilityicon'); - for (var i=0; i < event.attendees.length; i++) { - if (icons.get(i) && event.attendees[i].email && event.attendees[i].status != 'ACCEPTED') - check_freebusy_status(icons.get(i), event.attendees[i].email, event); + for (var i=0; i < event_attendees.length; i++) { + if (icons.get(i) && event_attendees[i].email && event_attendees[i].status != 'ACCEPTED') + check_freebusy_status(icons.get(i), event_attendees[i].email, event); } + + freebusy_needsupdate = false; }; // load free-busy status from server and update icon accordingly @@ -1372,8 +1592,10 @@ function rcube_calendar_ui(settings) // callback for clicks in all-day box dayClick: function(date, allDay, e, view) { var now = new Date().getTime(); - if (now - day_clicked_ts < 400 && day_clicked == date.getTime()) // emulate double-click on day - return event_edit_dialog('new', { start:date, end:date, allDay:allDay, calendar:me.selected_calendar }); + if (now - day_clicked_ts < 400 && day_clicked == date.getTime()) { // emulate double-click on day + var enddate = new Date(); enddate.setTime(date.getTime() + 86400000 - 60000); + return event_edit_dialog('new', { start:date, end:enddate, allDay:allDay, calendar:me.selected_calendar }); + } if (!ignore_click) { view.calendar.gotoDate(date); @@ -1563,14 +1785,17 @@ function rcube_calendar_ui(settings) // init event dialog $('#eventtabs').tabs({ show: function(event, ui) { - if (ui.panel.id == 'event-tab-3') + if (ui.panel.id == 'event-tab-3') { $('#edit-attendee-name').select(); + if (freebusy_needsupdate && me.selected_event) + update_freebusy_status(me.selected_event); + } } }); $('#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-enddate').datepicker('option', 'onSelect', event_times_changed).change(event_times_changed); - $('#edit-allday').click(function(){ $('#edit-starttime, #edit-endtime')[(this.checked?'hide':'show')](); }); + $('#edit-allday').click(function(){ $('#edit-starttime, #edit-endtime')[(this.checked?'hide':'show')](); event_times_changed(); }); // configure drop-down menu on time input fields based on jquery UI autocomplete $('#edit-starttime, #edit-endtime, input.edit-alarm-time') @@ -1630,13 +1855,17 @@ function rcube_calendar_ui(settings) event_freebusy_dialog(); }); - $('#shedule-freebusy-prev').button().click(function(){ render_freebusy_grid(-1); }); - $('#shedule-freebusy-next').button().click(function(){ render_freebusy_grid(1); }).parent().buttonset(); - + $('#shedule-freebusy-prev').html(bw.ie6 ? '<<' : '◄').button().click(function(){ render_freebusy_grid(-1); }); + $('#shedule-freebusy-next').html(bw.ie6 ? '>>' : '►').button().click(function(){ render_freebusy_grid(1); }).parent().buttonset(); + + $('#shedule-find-prev').button().click(function(){ freebusy_find_slot(-1); }); + $('#shedule-find-next').button().click(function(){ freebusy_find_slot(1); }); + $('#schedule-freebusy-wokinghours').click(function(){ + freebusy_ui.workinhoursonly = this.checked; $('#workinghourscss').remove(); if (this.checked) - $('').appendTo('head'); + $('').appendTo('head'); }); // add proprietary css styles if not IE diff --git a/plugins/calendar/config.inc.php.dist b/plugins/calendar/config.inc.php.dist index aa580071..a6b23415 100644 --- a/plugins/calendar/config.inc.php.dist +++ b/plugins/calendar/config.inc.php.dist @@ -52,6 +52,12 @@ $rcmail_config['calendar_first_day'] = 1; // first hour of the calendar (0-23) $rcmail_config['calendar_first_hour'] = 6; +// working hours begin +$rcmail_config['calendar_work_start'] = 6; + +// working hours end +$rcmail_config['calendar_work_end'] = 18; + // event categories $rcmail_config['calendar_categories'] = array( 'Personal' => 'c0c0c0', diff --git a/plugins/calendar/drivers/kolab/kolab_calendar.php b/plugins/calendar/drivers/kolab/kolab_calendar.php index 816a9444..50b06ed0 100644 --- a/plugins/calendar/drivers/kolab/kolab_calendar.php +++ b/plugins/calendar/drivers/kolab/kolab_calendar.php @@ -67,9 +67,11 @@ class kolab_calendar } else { $acl = $this->storage->_folder->getACL(); - $acl = $acl[$_SESSION['username']]; - if (strpos($acl, 'i') !== false) - $this->readonly = false; + if (is_array($acl)) { + $acl = $acl[$_SESSION['username']]; + if (strpos($acl, 'i') !== false) + $this->readonly = false; + } } } } diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php index 2b22320e..0430bb0d 100644 --- a/plugins/calendar/drivers/kolab/kolab_driver.php +++ b/plugins/calendar/drivers/kolab/kolab_driver.php @@ -693,9 +693,16 @@ class kolab_driver extends calendar_driver if (empty($email)/* || $end < time()*/) return false; + // map vcalendar fbtypes to internal values + $fbtypemap = array( + 'FREE' => calendar::FREEBUSY_FREE, + 'BUSY-TENTATIVE' => calendar::FREEBUSY_TENTATIVE, + 'X-OUT-OF-OFFICE' => calendar::FREEBUSY_OOF, + 'OOF' => calendar::FREEBUSY_OOF); + // ask kolab server first $fbdata = @file_get_contents(rcube_kolab::get_freebusy_url($email)); - + // get free-busy url from contacts if (!$fbdata) { $fburl = null; @@ -722,10 +729,12 @@ class kolab_driver extends calendar_driver $fbcal->parsevCalendar($fbdata); if ($fb = $fbcal->findComponent('vfreebusy')) { $result = array(); + $params = $fb->getExtraParams(); foreach ($fb->getBusyPeriods() as $from => $to) { if ($to == null) // no information, assume free break; - $result[] = array($from, $to); + $type = $params[$from]['FBTYPE']; + $result[] = array($from, $to, isset($fbtypemap[$type]) ? $fbtypemap[$type] : calendar::FREEBUSY_BUSY); } return $result; diff --git a/plugins/calendar/lib/calendar_ical.php b/plugins/calendar/lib/calendar_ical.php index 66cd9ee9..d2e06cb5 100644 --- a/plugins/calendar/lib/calendar_ical.php +++ b/plugins/calendar/lib/calendar_ical.php @@ -67,7 +67,7 @@ class calendar_ical $ical .= "DTSTART:" . gmdate('Ymd\THis\Z', $event['start']) . "\r\n"; $ical .= "DTEND:" . gmdate('Ymd\THis\Z', $event['end']) . "\r\n"; $ical .= "SUMMARY:" . self::escpape($event['title']) . "\r\n"; - $ical .= "DESCRIPTION:" . wordwrap(self::escpape($event['description']),75,'\r\n ') . "\r\n"; + $ical .= "DESCRIPTION:" . wordwrap(self::escpape($event['description']),75,"\r\n ") . "\r\n"; if (!empty($event['attendees'])){ diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php index 4d90826a..30d44ea5 100644 --- a/plugins/calendar/lib/calendar_ui.php +++ b/plugins/calendar/lib/calendar_ui.php @@ -592,10 +592,13 @@ class calendar_ui $table->add('attendees', html::tag('h3', 'boxtitle', $this->calendar->gettext('tabattendees')) . html::div('timesheader', ' ') . - html::div(array('id' => 'schedule-attendees-list'), '') + html::div(array('id' => 'schedule-attendees-list', 'class' => 'attendees-list'), '') ); $table->add('times', - html::div('scroll', html::tag('table', array('id' => 'schedule-freebusy-times', 'border' => 0, 'cellspacing' => 0), html::tag('thead') . html::tag('tbody'))) + html::div('scroll', + html::tag('table', array('id' => 'schedule-freebusy-times', 'border' => 0, 'cellspacing' => 0), html::tag('thead') . html::tag('tbody')) . + html::div(array('id' => 'schedule-event-time', 'style' => 'display:none'), ' ') + ) ); return $table->show($attrib); diff --git a/plugins/calendar/localization/en_US.inc b/plugins/calendar/localization/en_US.inc index a4a8a509..4b405ee5 100644 --- a/plugins/calendar/localization/en_US.inc +++ b/plugins/calendar/localization/en_US.inc @@ -31,6 +31,7 @@ $labels['edit'] = 'Edit'; $labels['save'] = 'Save'; $labels['remove'] = 'Remove'; $labels['cancel'] = 'Cancel'; +$labels['adobt'] = 'Adopt changes'; $labels['print'] = 'Print calendars'; $labels['title'] = 'Summary'; $labels['description'] = 'Description'; @@ -95,10 +96,14 @@ $labels['roleresource'] = 'Resource'; $labels['availfree'] = 'Free'; $labels['availbusy'] = 'Busy'; $labels['availunknown'] = 'Unknown'; +$labels['availtentative'] = 'Tentative'; $labels['availoutofoffice'] = 'Out of Office'; $labels['scheduletime'] = 'Available times'; $labels['sendnotifications'] = 'Send notifications'; -$labels['onlyworkinghours'] = 'Show only working hours'; +$labels['onlyworkinghours'] = 'Only working hours'; +$labels['prevslot'] = 'Previous Slot'; +$labels['nextslot'] = 'Next Slot'; +$labels['noslotfound'] = 'Unable to find a free time slot'; // event dialog tabs $labels['tabsummary'] = 'Summary'; diff --git a/plugins/calendar/skins/default/calendar.css b/plugins/calendar/skins/default/calendar.css index 47e47e8c..64669a92 100644 --- a/plugins/calendar/skins/default/calendar.css +++ b/plugins/calendar/skins/default/calendar.css @@ -36,6 +36,7 @@ body.calendarmain { #datepicker .ui-datepicker { width: 97% !important; + box-shadow: none; -moz-box-shadow: none; -webkit-box-shadow: none; } @@ -566,7 +567,7 @@ td.topalign { } .availability img.availabilityicon.loading { - background: url('images/loading-small.gif') top left no-repeat; + background: url('images/loading-small.gif') middle middle no-repeat; } #schedule-freebusy-times td.unknown, @@ -584,6 +585,11 @@ td.topalign { background: #c00; } +#schedule-freebusy-times td.tentative, +.availability img.availabilityicon.tentative { + background: #66d; +} + #schedule-freebusy-times td.out-of-office, .availability img.availabilityicon.out-of-office { background: #f0b400; @@ -591,6 +597,7 @@ td.topalign { #edit-attendees-legend { margin-top: 3em; + margin-bottom: 0.5em; } #edit-attendees-legend .legend { @@ -651,6 +658,7 @@ td.topalign { } #attendees-freebusy-table div.scroll { + position: relative; overflow: auto; } @@ -660,17 +668,33 @@ td.topalign { border-color: #ccc; } -#attendees-freebusy-table div.timesheader { - padding: 0.3em; +.attendees-list .attendee { + padding: 3px 4px 3px 20px; + background: url('images/attendee-status.gif') 2px -97px no-repeat; } -#schedule-attendees-list div.attendee { - padding: 3px 20px 3px 6px; +.attendees-list div.attendee { border-top: 1px solid #ccc; } -#schedule-attendees-list div.loading { - background: url('images/loading-small.gif') top right no-repeat; +.attendees-list span.attendee { + margin-right: 2em; +} + +.attendees-list .organizer { + background-position: 3px -77px; +} + +.attendees-list .opt-participant { + background-position: 2px -117px; +} + +.attendees-list .chair { + background-position: 2px -137px; +} + +.attendees-list .loading { + background: url('images/loading-small.gif') 1px 50% no-repeat; } #schedule-freebusy-times { @@ -688,6 +712,7 @@ td.topalign { border-width: 0 1px 0 1px; } +#attendees-freebusy-table div.timesheader, #schedule-freebusy-times tr.times td { min-width: 30px; font-size: 9px; @@ -695,9 +720,19 @@ td.topalign { text-align: center; } +#schedule-event-time { + position: absolute; + border: 2px solid #333; + background: #777; + background: rgba(60, 60, 60, 0.6); + opacity: 0.5; + border-radius: 4px; + cursor: move; +} + #eventfreebusy .schedule-options { position: relative; - margin-bottom: 2em; + margin-bottom: 1.5em; } #eventfreebusy .schedule-buttons { @@ -706,6 +741,15 @@ td.topalign { right: 0; } +#eventfreebusy .schedule-find-buttons { + padding-bottom:0.5em; +} + +#eventfreebusy .schedule-find-buttons button { + min-width: 9em; + text-align: center; +} + span.edit-alarm-set { white-space: nowrap; } diff --git a/plugins/calendar/skins/default/iehacks.css b/plugins/calendar/skins/default/iehacks.css index fa282176..31f82d78 100644 --- a/plugins/calendar/skins/default/iehacks.css +++ b/plugins/calendar/skins/default/iehacks.css @@ -43,3 +43,12 @@ html #calendartoolbar a.buttonPas { .fc-header-title h2 { font-size: 16px; } + +#schedule-event-time { + filter: alpha(opacity=40); +} + +#eventfreebusy .schedule-buttons, +#edit-attendees-form #edit-attendee-schedule { + right: 0.6em; +} \ No newline at end of file diff --git a/plugins/calendar/skins/default/images/attendee-status.gif b/plugins/calendar/skins/default/images/attendee-status.gif index a5f65a02..5c08aae3 100644 Binary files a/plugins/calendar/skins/default/images/attendee-status.gif and b/plugins/calendar/skins/default/images/attendee-status.gif differ diff --git a/plugins/calendar/skins/default/templates/calendar.html b/plugins/calendar/skins/default/templates/calendar.html index bf83f030..de0419cc 100644 --- a/plugins/calendar/skins/default/templates/calendar.html +++ b/plugins/calendar/skins/default/templates/calendar.html @@ -188,25 +188,42 @@
- +  
- - +
-
- -   - +
+
+ +   + +
+
+ +   + +
-
- -   - +
+
+ + +
+
+ +
+
+
+ + + + +
diff --git a/plugins/calendar/skins/default/templates/freebusylegend.html b/plugins/calendar/skins/default/templates/freebusylegend.html index c3d0a86e..5cc01a18 100644 --- a/plugins/calendar/skins/default/templates/freebusylegend.html +++ b/plugins/calendar/skins/default/templates/freebusylegend.html @@ -1,6 +1,7 @@
+
diff --git a/plugins/kolab_addressbook/kolab_addressbook.js b/plugins/kolab_addressbook/kolab_addressbook.js index cdb525d1..9cb1756e 100644 --- a/plugins/kolab_addressbook/kolab_addressbook.js +++ b/plugins/kolab_addressbook/kolab_addressbook.js @@ -191,7 +191,7 @@ rcube_webmail.prototype.book_update = function(data, old) level = olddata.realname.split(this.env.delimiter).length - data.realname.split(this.env.delimiter).length; // update (realname and ID of) subfolders for (n in sources) { - if (n.indexOf(old) == 0) { + if (n != data.id && n.indexOf(old) == 0) { // new ID id = data.id + '-' + n.substr(old.length); name = sources[n].name; diff --git a/plugins/kolab_addressbook/kolab_addressbook.php b/plugins/kolab_addressbook/kolab_addressbook.php index 5432ffac..c6178920 100644 --- a/plugins/kolab_addressbook/kolab_addressbook.php +++ b/plugins/kolab_addressbook/kolab_addressbook.php @@ -27,7 +27,7 @@ */ class kolab_addressbook extends rcube_plugin { - public $task = 'mail|settings|addressbook'; + public $task = 'mail|settings|addressbook|calendar'; private $folders; private $sources; @@ -54,6 +54,7 @@ class kolab_addressbook extends rcube_plugin // register hooks $this->add_hook('addressbooks_list', array($this, 'address_sources')); $this->add_hook('addressbook_get', array($this, 'get_address_book')); + $this->add_hook('config_get', array($this, 'config_get')); if ($this->rc->task == 'addressbook') { $this->add_texts('localization'); @@ -74,10 +75,6 @@ class kolab_addressbook extends rcube_plugin $this->add_hook('preferences_list', array($this, 'prefs_list')); $this->add_hook('preferences_save', array($this, 'prefs_save')); } - // extend list of address sources to be used for autocompletion - else if ($this->rc->task == 'mail' && $this->rc->action == 'autocomplete') { - $this->autocomplete_sources(); - } } @@ -149,16 +146,22 @@ class kolab_addressbook extends rcube_plugin /** - * Setts autocomplete_addressbooks option according to - * kolab_addressbook_prio setting. + * Sets autocomplete_addressbooks option according to + * kolab_addressbook_prio setting extending list of address sources + * to be used for autocompletion. */ - public function autocomplete_sources() + public function config_get($args) { + if ($args['name'] != 'autocomplete_addressbooks') { + return $args; + } + // Load configuration $this->load_config(); $abook_prio = (int) $this->rc->config->get('kolab_addressbook_prio'); - $sources = (array) $this->rc->config->get('autocomplete_addressbooks', array()); + // here we cannot use rc->config->get() + $sources = $GLOBALS['CONFIG']['autocomplete_addressbooks']; // Disable all global address books // Assumes that all non-kolab_addressbook sources are global @@ -166,6 +169,10 @@ class kolab_addressbook extends rcube_plugin $sources = array(); } + if (!is_array($sources)) { + $sources = array(); + } + $kolab_sources = array(); foreach ($this->_list_sources() as $abook_id => $abook) { if (!in_array($abook_id, $sources)) @@ -180,9 +187,11 @@ class kolab_addressbook extends rcube_plugin else { $sources = array_merge($sources, $kolab_sources); } - - $this->rc->config->set('autocomplete_addressbooks', $sources); } + + $args['result'] = $sources; + + return $args; } diff --git a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php index b4c72a4f..6cc0289b 100644 --- a/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php +++ b/plugins/kolab_addressbook/lib/rcube_kolab_contacts.php @@ -128,11 +128,13 @@ class rcube_kolab_contacts extends rcube_addressbook } else { $acl = $this->storagefolder->getACL(); - $acl = $acl[$_SESSION['username']]; - if (strpos($acl, 'i') !== false) - $this->readonly = false; - if (strpos($acl, 'a') !== false || strpos($acl, 'x') !== false) - $this->editable = true; + if (is_array($acl)) { + $acl = $acl[$_SESSION['username']]; + if (strpos($acl, 'i') !== false) + $this->readonly = false; + if (strpos($acl, 'a') !== false || strpos($acl, 'x') !== false) + $this->editable = true; + } } } } @@ -363,6 +365,8 @@ class rcube_kolab_contacts extends rcube_addressbook $search = $value; } + $s_len = strlen($search); + foreach ((array)$contact[$col] as $val) { // composite field, e.g. address if (is_array($val)) { @@ -370,7 +374,9 @@ class rcube_kolab_contacts extends rcube_addressbook } $val = mb_strtolower($val); - if (($strict && $val == $search) || (!$strict && strpos($val, $search) !== false)) { + if (($strict && $val == $search) + || (!$strict && $s_len && strpos($val, $search) !== false) + ) { if (!$advanced) { $this->filter['ids'][] = $id; break 2; @@ -892,6 +898,31 @@ class rcube_kolab_contacts extends rcube_addressbook return false; } + /** + * Check the given data before saving. + * If input not valid, the message to display can be fetched using get_error() + * + * @param array Associative array with contact data to save + * + * @return boolean True if input is valid, False if not. + */ + public function validate($save_data) + { + // validate e-mail addresses + $valid = parent::validate($save_data); + + // require at least one e-mail address (syntax check is already done) + if ($valid) { + if (!strlen($save_data['name']) + && !array_filter($this->get_col_values('email', $save_data, true)) + ) { + $this->set_error('warning', 'kolab_addressbook.noemailnamewarning'); + $valid = false; + } + } + + return $valid; + } /** * Establishes a connection to the Kolab_Data object for accessing contact data @@ -903,7 +934,6 @@ class rcube_kolab_contacts extends rcube_addressbook } } - /** * Establishes a connection to the Kolab_Data object for accessing groups data */ @@ -914,7 +944,6 @@ class rcube_kolab_contacts extends rcube_addressbook } } - /** * Simply fetch all records and store them in private member vars */ @@ -941,7 +970,6 @@ class rcube_kolab_contacts extends rcube_addressbook } } - /** * Callback function for sorting contacts */ @@ -950,7 +978,6 @@ class rcube_kolab_contacts extends rcube_addressbook return strcasecmp($a['name'], $b['name']); } - /** * Read distribution-lists AKA groups from server */ @@ -975,7 +1002,6 @@ class rcube_kolab_contacts extends rcube_addressbook } } - /** * Map fields from internal Kolab_Format to Roundcube contact format */ @@ -1026,6 +1052,9 @@ class rcube_kolab_contacts extends rcube_addressbook return array_filter($out); } + /** + * Map fields from Roundcube format to internal Kolab_Format + */ private function _from_rcube_contact($contact) { $object = array(); diff --git a/plugins/kolab_addressbook/localization/en_US.inc b/plugins/kolab_addressbook/localization/en_US.inc index ce90f60a..f3cb17ea 100644 --- a/plugins/kolab_addressbook/localization/en_US.inc +++ b/plugins/kolab_addressbook/localization/en_US.inc @@ -25,12 +25,13 @@ $labels['globalonly'] = 'Global address book(s) only'; $messages['bookdeleteconfirm'] = 'Do you really want to delete the selected address book and all contacts in it?'; $messages['bookdeleting'] = 'Deleting address book...'; $messages['booksaving'] = 'Saving address book...'; -$messages['bookdeleted'] = 'Address book deleted successfully'; -$messages['bookupdated'] = 'Address book updated successfully'; -$messages['bookcreated'] = 'Address book created successfully'; -$messages['bookdeleteerror'] = 'An error occured while deleting address book'; -$messages['bookupdateerror'] = 'An error occured while updating address book'; -$messages['bookcreateerror'] = 'An error occured while creating address book'; -$messages['nobooknamewarning'] = 'Please, enter address book name'; +$messages['bookdeleted'] = 'Address book deleted successfully.'; +$messages['bookupdated'] = 'Address book updated successfully.'; +$messages['bookcreated'] = 'Address book created successfully.'; +$messages['bookdeleteerror'] = 'An error occured while deleting address book.'; +$messages['bookupdateerror'] = 'An error occured while updating address book.'; +$messages['bookcreateerror'] = 'An error occured while creating address book.'; +$messages['nobooknamewarning'] = 'Please, enter address book name.'; +$messages['noemailnamewarning'] = 'Please, enter email address or contact name.'; ?> diff --git a/plugins/kolab_addressbook/localization/pl_PL.inc b/plugins/kolab_addressbook/localization/pl_PL.inc index 61887231..e2735783 100644 --- a/plugins/kolab_addressbook/localization/pl_PL.inc +++ b/plugins/kolab_addressbook/localization/pl_PL.inc @@ -25,12 +25,13 @@ $labels['parentbook'] = 'Książka nadrzędna'; $messages['bookdeleteconfirm'] = 'Czy na pewno chcesz usunąć wybraną książkę i wszystkie kontakty w niej zapisane?'; $messages['bookdeleting'] = 'Usuwanie książki adresowej...'; $messages['booksaving'] = 'Zapisywanie książki adresowej...'; -$messages['bookdeleted'] = 'Książka adresowa została usunięta'; -$messages['bookupdated'] = 'Książka adresowa została zaktualizowana'; -$messages['bookcreated'] = 'Książka adresowa została utworzona'; -$messages['bookdeleteerror'] = 'Wystąpił błąd podczas usuwania książki adresowej'; -$messages['bookupdateerror'] = 'Wystąpił błąd podczas zmiany książki adresowej'; -$messages['bookcreateerror'] = 'Wystąpił błąd podczas tworzenia książki adresowej'; -$messages['nobooknamewarning'] = 'Proszę podać nazwę książki adresowej'; +$messages['bookdeleted'] = 'Książka adresowa została usunięta.'; +$messages['bookupdated'] = 'Książka adresowa została zaktualizowana.'; +$messages['bookcreated'] = 'Książka adresowa została utworzona.'; +$messages['bookdeleteerror'] = 'Wystąpił błąd podczas usuwania książki adresowej.'; +$messages['bookupdateerror'] = 'Wystąpił błąd podczas zmiany książki adresowej.'; +$messages['bookcreateerror'] = 'Wystąpił błąd podczas tworzenia książki adresowej.'; +$messages['nobooknamewarning'] = 'Proszę podać nazwę książki adresowej.'; +$messages['noemailnamewarning'] = 'Proszę podać adres email lub nazwę kontaktu.'; ?> diff --git a/plugins/kolab_core/rcube_kolab.php b/plugins/kolab_core/rcube_kolab.php index 74b46a86..10cdb36a 100644 --- a/plugins/kolab_core/rcube_kolab.php +++ b/plugins/kolab_core/rcube_kolab.php @@ -29,6 +29,8 @@ class rcube_kolab private static $horde_auth; private static $config; private static $ready = false; + private static $list; + private static $cache; /** @@ -72,9 +74,11 @@ class rcube_kolab // Re-set LDAP/IMAP host config $ldap = array('server' => 'ldap://' . $_SESSION['imap_host'] . ':389'); $imap = array('server' => $_SESSION['imap_host'], 'port' => $_SESSION['imap_port']); + $freebusy = array('server' => $_SESSION['imap_host']); $conf['kolab']['ldap'] = array_merge($ldap, (array)$conf['kolab']['ldap']); $conf['kolab']['imap'] = array_merge($imap, (array)$conf['kolab']['imap']); + $conf['kolab']['freebusy'] = array_merge($freebusy, (array)$conf['kolab']['freebusy']); self::$config = &$conf; // pass the current IMAP authentication credentials to the Horde auth system @@ -88,19 +92,57 @@ class rcube_kolab ); Auth::setCredential('password', $pwd); self::$ready = true; - - // Register shutdown function for storing folders cache in session - // This is already required, because Roundcube session handler - // saves data into DB before Horde's shutdown function is called - if (!empty($conf['kolab']['imap']['cache_folders'])) { - $rcmail->add_shutdown_function(array('rcube_kolab', 'save_folders_cache')); - } } NLS::setCharset('UTF-8'); String::setDefaultCharset('UTF-8'); } + /** + * Get instance of Kolab_List object + * + * @return object Kolab_List Folders list object + */ + public static function get_folders_list() + { + self::setup(); + + if (self::$list) + return self::$list; + + if (!self::$ready) + return null; + + $rcmail = rcmail::get_instance(); + $imap_cache = $rcmail->config->get('imap_cache'); + + if ($imap_cache) { + self::$cache = $rcmail->get_cache('IMAP', $imap_cache); + self::$list = self::$cache->get('mailboxes.kolab'); + + // Disable Horde folders caching, we're using our own cache + self::$config['kolab']['imap']['cache_folders'] = false; + // Register shutdown function for saving folders list cache + $rcmail->add_shutdown_function(array('rcube_kolab', 'save_folders_list')); + } + + if (empty(self::$list)) { + self::$list = Kolab_List::singleton(); + } + + return self::$list; + } + + /** + * Store Kolab_List instance in Roundcube cache + */ + public static function save_folders_list() + { + if (self::$cache && self::$list) { + self::$cache->set('mailboxes.kolab', self::$list); + } + } + /** * Get instance of a Kolab (XML) format object * @@ -123,8 +165,7 @@ class rcube_kolab */ public static function get_folders($type) { - self::setup(); - $kolab = Kolab_List::singleton(); + $kolab = self::get_folders_list(); return self::$ready ? $kolab->getByType($type) : array(); } @@ -136,9 +177,8 @@ class rcube_kolab */ public static function get_folder($folder) { - self::setup(); - $kolab = Kolab_List::singleton(); - return self::$ready ? $kolab->getFolder($folder) : null; + $kolab = self::get_folders_list(); + return self::$ready ? $kolab->getFolder($folder) : null; } /** @@ -151,8 +191,7 @@ class rcube_kolab */ public static function get_storage($folder, $data_type = null) { - self::setup(); - $kolab = Kolab_List::singleton(); + $kolab = self::get_folders_list(); return self::$ready ? $kolab->getFolder($folder)->getData($data_type) : null; } @@ -175,18 +214,6 @@ class rcube_kolab unset($_SESSION['__auth']); } - /** - * Save Horde's folders cache in session (workaround shoutdown function issue) - */ - public static function save_folders_cache() - { - require_once 'Horde/SessionObjects.php'; - - $kolab = Kolab_List::singleton(); - $session = Horde_SessionObjects::singleton(); - $session->overwrite('kolab_folderlist', $kolab, false); - } - /** * Creates folder ID from folder name * @@ -208,8 +235,7 @@ class rcube_kolab */ public static function folder_delete($name) { - self::setup(); - $kolab = Kolab_List::singleton(); + $kolab = self::get_folders_list(); $folder = $kolab->getFolder($name); $result = $folder->delete(); @@ -232,8 +258,7 @@ class rcube_kolab */ public static function folder_create($name, $type=null, $default=false) { - self::setup(); - $kolab = Kolab_List::singleton(); + $kolab = self::get_folders_list(); $folder = new Kolab_Folder(); $folder->setList($kolab); @@ -261,17 +286,26 @@ class rcube_kolab */ public static function folder_rename($oldname, $newname) { - self::setup(); - $kolab = Kolab_List::singleton(); + $kolab = self::get_folders_list(); $folder = $kolab->getFolder($oldname); $folder->setFolder($newname); + + // We're not using $folder->save() because some caching issues $result = $kolab->rename($folder); if (is_a($result, 'PEAR_Error')) { return false; } + // need to re-set some properties + $folder->name = $folder->new_name; + $folder->new_name = null; + $folder->_title = null; + $folder->_owner = null; + // resetting _data prevents from some wierd cache unserialization issue + $folder->_data = null; + return true; }