From 83d5c9f7f529e97561f1c02224e5a482af261d37 Mon Sep 17 00:00:00 2001 From: Aleksander Machniak Date: Fri, 23 Jun 2023 14:38:10 +0200 Subject: [PATCH] Fix handling of kolab_freebusy_server=false (and other empty values) - Cleanup code - Hide availability features in the Resources tab --- plugins/calendar/calendar.php | 1 + plugins/calendar/calendar_ui.js | 117 ++++++++++-------- .../calendar/drivers/kolab/kolab_driver.php | 8 +- plugins/libkolab/lib/kolab_storage.php | 19 +-- plugins/libkolab/lib/kolab_storage_dav.php | 2 +- plugins/libkolab/lib/kolab_storage_folder.php | 9 +- 6 files changed, 88 insertions(+), 68 deletions(-) diff --git a/plugins/calendar/calendar.php b/plugins/calendar/calendar.php index f3e8b3c6..345277d6 100644 --- a/plugins/calendar/calendar.php +++ b/plugins/calendar/calendar.php @@ -342,6 +342,7 @@ class calendar extends rcube_plugin $this->rc->output->set_env('timezone', $this->timezone->getName()); $this->rc->output->set_env('calendar_driver', $this->rc->config->get('calendar_driver'), false); $this->rc->output->set_env('calendar_resources', (bool)$this->rc->config->get('calendar_resources_driver')); + $this->rc->output->set_env('calendar_resources_freebusy', !empty($this->rc->config->get('kolab_freebusy_server'))); $this->rc->output->set_env('identities-selector', $this->ui->identity_select([ 'id' => 'edit-identities-list', 'aria-label' => $this->gettext('roleorganizer'), diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js index 494a5e4b..8edda69d 100644 --- a/plugins/calendar/calendar_ui.js +++ b/plugins/calendar/calendar_ui.js @@ -924,13 +924,19 @@ function rcube_calendar_ui(settings) $('#edit-tab-attachments')[(calendar.attachments?'show':'hide')](); $('#eventedit:not([data-notabs])').tabs('option', 'active', 0); // Larry - // show/hide tabs according to calendar's feature support and activate first tab (Ellastic) + // show/hide tabs according to calendar's feature support and activate first tab (Elastic) $('li > a[href="#event-panel-attendees"]').parent()[(calendar.attendees?'show':'hide')](); $('li > a[href="#event-panel-resources"]').parent()[(rcmail.env.calendar_resources?'show':'hide')](); $('li > a[href="#event-panel-attachments"]').parent()[(calendar.attachments?'show':'hide')](); if ($('#eventedit').data('notabs')) $('#eventedit li.nav-item:first-child a').tab('show'); + if (!rcmail.env.calendar_resources_freebusy) { + // With no freebusy setup, some features needs to be hidden + // TODO: Show "Find resources" dialog, but with Availability tab hidden + $('#event-panel-resources').find('#edit-resource-find,#edit-attendees-legend,td.availability,th.availability').hide(); + } + // hack: set task to 'calendar' to make all dialog actions work correctly var comm_path_before = rcmail.env.comm_path; rcmail.env.comm_path = comm_path_before.replace(/_task=[a-z]+/, '_task=calendar'); @@ -1182,13 +1188,13 @@ function rcube_calendar_ui(settings) { var $dialog = $('#eventfreebusy'), event = me.selected_event; - + if ($dialog.is(':ui-dialog')) $dialog.dialog('close'); - + if (!event_attendees.length) return false; - + // set form elements var allday = $('#edit-allday').get(0); var end = 'toDate' in event.end ? event.end : moment(event.end); @@ -1199,7 +1205,7 @@ function rcube_calendar_ui(settings) freebusy_ui.starttime = $('#schedule-starttime').val(format_date(start, settings.time_format)).show(); freebusy_ui.enddate = $('#schedule-enddate').val(format_date(end, settings.date_format)); freebusy_ui.endtime = $('#schedule-endtime').val(format_date(end, settings.time_format)).show(); - + if (allday.checked) { freebusy_ui.starttime.val("12:00").hide(); freebusy_ui.endtime.val("13:00").hide(); @@ -1211,7 +1217,7 @@ function rcube_calendar_ui(settings) 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() + DAY_MS); - + freebusy_data = { required:{}, all:{} }; freebusy_ui.loading = 1; // prevent render_freebusy_grid() to load data yet freebusy_ui.numdays = Math.max(allday.checked ? 14 : 1, Math.ceil(duration * 2 / 86400)); @@ -1219,7 +1225,7 @@ function rcube_calendar_ui(settings) freebusy_ui.start = fb_start; freebusy_ui.end = new Date(freebusy_ui.start.getTime() + DAY_MS * freebusy_ui.numdays); render_freebusy_grid(0); - + // render list of attendees freebusy_ui.attendees = {}; var domid, dispname, data, role_html, list_html = ''; @@ -1229,15 +1235,15 @@ function rcube_calendar_ui(settings) domid = String(data.email).replace(rcmail.identifier_expr, ''); role_html = ' '; list_html += '
' + role_html + dispname + '
'; - + // clone attendees data for local modifications freebusy_ui.attendees[i] = freebusy_ui.attendees[domid] = $.extend({}, data); } - + // add total row list_html += '
 
'; list_html += '
' + rcmail.gettext('reqallattendees','calendar') + '
'; - + $('#schedule-attendees-list').html(list_html) .unbind('click.roleicons') .bind('click.roleicons', function(e) { @@ -1251,7 +1257,7 @@ function rcube_calendar_ui(settings) j = (j+1) % roles.length; attendee.role = roles[j]; $(e.target).parent().attr('class', 'attendee '+String(attendee.role).toLowerCase()); - + // update total display if required-status changed if (req != (roles[j] != 'OPT-PARTICIPANT' && roles[j] != 'NON-PARTICIPANT')) { compute_freebusy_totals(); @@ -1259,7 +1265,7 @@ function rcube_calendar_ui(settings) } } } - + return false; }); @@ -1321,12 +1327,12 @@ function rcube_calendar_ui(settings) minWidth: 640, width: 850 }).show(); - + // adjust dialog size to fit grid without scrolling var gridw = $('#schedule-freebusy-times').width(); var overflow = gridw - $('#attendees-freebusy-table td.times').width(); me.dialog_resize($dialog.get(0), $dialog.height() + (bw.ie ? 20 : 0), 800 + Math.max(0, overflow)); - + // fetch data from server freebusy_ui.loading = 0; load_freebusy_data(freebusy_ui.start, freebusy_ui.interval); @@ -1338,17 +1344,17 @@ function rcube_calendar_ui(settings) if (delta) { freebusy_ui.start.setTime(freebusy_ui.start.getTime() + DAY_MS * delta); fix_date(freebusy_ui.start); - + // skip weekends if in workinhoursonly-mode if (Math.abs(delta) == 1 && freebusy_ui.workinhoursonly) { while (is_weekend(freebusy_ui.start)) freebusy_ui.start.setTime(freebusy_ui.start.getTime() + DAY_MS * delta); fix_date(freebusy_ui.start); } - + freebusy_ui.end = new Date(freebusy_ui.start.getTime() + DAY_MS * freebusy_ui.numdays); } - + var dayslots = Math.floor(1440 / freebusy_ui.interval); var date_format = 'ddd '+ (dayslots <= 2 ? settings.date_short : settings.date_format); var lastdate, datestr, css, @@ -1368,17 +1374,17 @@ function rcube_calendar_ui(settings) dates_row += '' + Q(datestr) + ''; lastdate = datestr; } - + // set css class according to working hours css = is_weekend(curdate) || (freebusy_ui.interval <= 60 && !is_workinghour(curdate)) ? 'offhours' : 'workinghours'; times_row += '' + Q(allday ? rcmail.gettext('all-day','calendar') : format_date(curdate, settings.time_format)) + ''; slots_row += ' '; - + t += interval * 60000; } dates_row += ''; times_row += ''; - + // render list of attendees var domid, data, list_html = '', times_html = ''; for (var i=0; i < event_attendees.length; i++) { @@ -1386,15 +1392,15 @@ function rcube_calendar_ui(settings) domid = String(data.email).replace(rcmail.identifier_expr, ''); times_html += '' + slots_row + ''; } - + // add line for all/required attendees times_html += ''; times_html += '' + slots_row + ''; - + var table = $('#schedule-freebusy-times'); table.children('thead').html(dates_row + times_row); table.children('tbody').html(times_html); - + // initialize event handlers on grid if (!freebusy_ui.grid_events) { freebusy_ui.grid_events = true; @@ -1412,7 +1418,7 @@ function rcube_calendar_ui(settings) } }); } - + // if we have loaded free-busy data, show it if (!freebusy_ui.loading) { if (freebusy_ui.start < freebusy_data.start || freebusy_ui.end > freebusy_data.end || freebusy_ui.interval != freebusy_data.interval) { @@ -1425,12 +1431,12 @@ function rcube_calendar_ui(settings) } } } - + // 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() { @@ -1549,7 +1555,7 @@ function rcube_calendar_ui(settings) domid = String(email).replace(rcmail.identifier_expr, ''); $('#rcmli' + domid).addClass('loading'); freebusy_ui.loading++; - + $.ajax({ type: 'GET', dataType: 'json', @@ -1557,7 +1563,7 @@ function rcube_calendar_ui(settings) data: { email:email, start:date2servertime(clone_date(start, 1)), end:date2servertime(clone_date(end, 2)), interval:interval, _remote:1 }, success: function(data) { freebusy_ui.loading--; - + // find attendee var i, attendee = null; for (i=0; i < event_attendees.length; i++) { @@ -1566,7 +1572,7 @@ function rcube_calendar_ui(settings) break; } } - + // copy data to member var var ts, status, req = attendee.role != 'OPT-PARTICIPANT', @@ -1582,13 +1588,13 @@ function rcube_calendar_ui(settings) status = data.slots.charAt(i); freebusy_data[data.email][ts] = status start = new Date(start.getTime() + data.interval * 60000); - + // set totals if (!freebusy_data.required[ts]) freebusy_data.required[ts] = [0,0,0,0]; if (req) freebusy_data.required[ts][status]++; - + if (!freebusy_data.all[ts]) freebusy_data.all[ts] = [0,0,0,0]; freebusy_data.all[ts][status]++; @@ -1597,44 +1603,44 @@ function rcube_calendar_ui(settings) // hide loading indicator var domid = String(data.email).replace(rcmail.identifier_expr, ''); $('#rcmli' + domid).removeClass('loading'); - + // update display update_freebusy_display(data.email); } }); - + // count required attendees if (freebusy_ui.attendees[i].role != 'OPT-PARTICIPANT') freebusy_ui.numrequired++; } } }; - + // re-calculate total status after role change var compute_freebusy_totals = function() { freebusy_ui.numrequired = 0; freebusy_data.all = []; freebusy_data.required = []; - + var email, req, status; for (var i=0; i < event_attendees.length; i++) { if (!(email = event_attendees[i].email)) continue; - + req = freebusy_ui.attendees[i].role != 'OPT-PARTICIPANT'; if (req) freebusy_ui.numrequired++; - + for (var ts in freebusy_data[email]) { if (!freebusy_data.required[ts]) freebusy_data.required[ts] = [0,0,0,0]; if (!freebusy_data.all[ts]) freebusy_data.all[ts] = [0,0,0,0]; - + status = freebusy_data[email][ts]; freebusy_data.all[ts][status]++; - + if (req) freebusy_data.required[ts][status]++; } @@ -1787,7 +1793,7 @@ function rcube_calendar_ui(settings) break; } } - + // occupied slot if (!candidatestart) { slot += Math.max(0, intvlslots - candidatecount - 1) * sinterval * dir; @@ -1796,9 +1802,9 @@ function rcube_calendar_ui(settings) } else if (dir < 0) candidatestart = slot; - + candidatecount++; - + // if candidate is big enough, this is it! if (candidatecount == numslots) { 'toDate' in event.start ? (event.start = new Date(candidatestart)) : event.start.setTime(candidatestart); @@ -1859,7 +1865,7 @@ function rcube_calendar_ui(settings) for (var i=0; i < names.length; i++) { email = name = ''; item = $.trim(names[i]); - + if (!item.length) { continue; } // address in brackets without name (do nothing) @@ -1881,7 +1887,7 @@ function rcube_calendar_ui(settings) rcmail.alert_dialog(rcmail.gettext('noemailwarning')); } } - + return success; }; @@ -2006,10 +2012,10 @@ function rcube_calendar_ui(settings) if (email = icon.attr('data-email')) check_freebusy_status(icon, email, event); }); - + freebusy_ui.needsupdate = false; }; - + // load free-busy status from server and update icon accordingly var check_freebusy_status = function(icon, email, event) { @@ -2018,9 +2024,9 @@ function rcube_calendar_ui(settings) $(icon).attr('class', 'availabilityicon unknown'); return; } - + icon = $(icon).attr('class', 'availabilityicon loading'); - + $.ajax({ type: 'GET', dataType: 'html', @@ -2035,7 +2041,7 @@ function rcube_calendar_ui(settings) } }); }; - + // remove an attendee from the list var remove_attendee = function(elem, id) { @@ -3369,20 +3375,21 @@ function rcube_calendar_ui(settings) if (q != '') { var id = 'search-'+q; var sources = []; - + if (me.quickview_active) reset_quickview(); - + if (this._search_message) rcmail.hide_message(this._search_message); - + $.each(fc.fullCalendar('getEventSources'), function() { this.url = this.url.replace(/&q=.+/, '') + '&q=' + urlencode(q); me.calendars[this.id].url = this.url; sources.push(this.id); }); + id += '@'+sources.join(','); - + // ignore if query didn't change if (this.search_request == id) { return; @@ -3391,10 +3398,10 @@ function rcube_calendar_ui(settings) else if (!this.search_request) { this.default_view = fc.fullCalendar('getView').name; } - + this.search_request = id; this.search_query = q; - + // change to list view fc.fullCalendar('changeView', 'list'); diff --git a/plugins/calendar/drivers/kolab/kolab_driver.php b/plugins/calendar/drivers/kolab/kolab_driver.php index 01d6f45e..4d7cf5f8 100644 --- a/plugins/calendar/drivers/kolab/kolab_driver.php +++ b/plugins/calendar/drivers/kolab/kolab_driver.php @@ -2016,6 +2016,12 @@ class kolab_driver extends calendar_driver return false; } + $url = $this->storage->get_freebusy_url($email); + + if (empty($url)) { + return false; + } + // map vcalendar fbtypes to internal values $fbtypemap = [ 'FREE' => calendar::FREEBUSY_FREE, @@ -2031,7 +2037,7 @@ class kolab_driver extends calendar_driver 'follow_redirects' => true, ]; - $request = libkolab::http_request($this->storage->get_freebusy_url($email), 'GET', $request_config); + $request = libkolab::http_request($url, 'GET', $request_config); $response = $request->send(); // authentication required diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php index 03cd404e..d4307a6c 100644 --- a/plugins/libkolab/lib/kolab_storage.php +++ b/plugins/libkolab/lib/kolab_storage.php @@ -279,11 +279,12 @@ class kolab_storage { $rcmail = rcube::get_instance(); - $url = 'https://' . $_SESSION['imap_host'] . '/freebusy'; - $url = $rcmail->config->get('kolab_freebusy_server', $url); - $url = rcube_utils::resolve_url($url); + if ($url = $rcmail->config->get('kolab_freebusy_server')) { + $url = rcube_utils::resolve_url($url); + $url = unslashify($url); + } - return unslashify($url); + return $url; } /** @@ -293,10 +294,16 @@ class kolab_storage * @param object DateTime Start of the query range (optional) * @param object DateTime End of the query range (optional) * - * @return string Fully qualified URL to query free/busy data + * @return ?string Fully qualified URL to query free/busy data */ public static function get_freebusy_url($email, $start = null, $end = null) { + $url = self::get_freebusy_server(); + + if (empty($url)) { + return null; + } + $query = ''; $param = array(); $utc = new \DateTimeZone('UTC'); @@ -317,8 +324,6 @@ class kolab_storage $query = '?' . http_build_query($param); } - $url = self::get_freebusy_server(); - if (strpos($url, '%u')) { // Expected configured full URL, just replace the %u variable // Note: Cyrus v3 Free-Busy service does not use .ifb extension diff --git a/plugins/libkolab/lib/kolab_storage_dav.php b/plugins/libkolab/lib/kolab_storage_dav.php index c8accfc3..086df3db 100644 --- a/plugins/libkolab/lib/kolab_storage_dav.php +++ b/plugins/libkolab/lib/kolab_storage_dav.php @@ -149,7 +149,7 @@ class kolab_storage_dav * @param object DateTime Start of the query range (optional) * @param object DateTime End of the query range (optional) * - * @return string Fully qualified URL to query free/busy data + * @return ?string Fully qualified URL to query free/busy data */ public static function get_freebusy_url($email, $start = null, $end = null) { diff --git a/plugins/libkolab/lib/kolab_storage_folder.php b/plugins/libkolab/lib/kolab_storage_folder.php index 766975f8..22f56819 100644 --- a/plugins/libkolab/lib/kolab_storage_folder.php +++ b/plugins/libkolab/lib/kolab_storage_folder.php @@ -1099,13 +1099,14 @@ class kolab_storage_folder extends kolab_storage_folder_api { $owner = $this->get_owner(); $result = false; + $url = kolab_storage::get_freebusy_server(); - switch($this->type) { + switch ($this->type) { case 'event': - if ($this->get_namespace() == 'personal') { + if ($url && $this->get_namespace() == 'personal') { $result = $this->trigger_url( sprintf('%s/trigger/%s/%s.pfb', - kolab_storage::get_freebusy_server(), + $url, urlencode($owner), urlencode($this->imap->mod_folder($this->name)) ), @@ -1119,7 +1120,7 @@ class kolab_storage_folder extends kolab_storage_folder_api return true; } - if ($result && is_object($result) && is_a($result, 'PEAR_Error')) { + if ($result instanceof PEAR_Error) { return PEAR::raiseError( sprintf("Failed triggering folder %s. Error was: %s", $this->name, $result->getMessage()) );