Page Menu
Home
Phorge
Search
Configure Global Search
Log In
Files
F223415
No One
Temporary
Actions
View File
Edit File
Delete File
View Transforms
Subscribe
Mute Notifications
Award Token
Flag For Later
Size
481 KB
Referenced Files
None
Subscribers
None
View Options
This file is larger than 256 KB, so syntax highlighting was skipped.
diff --git a/plugins/calendar/calendar_ui.js b/plugins/calendar/calendar_ui.js
index 7e2f25c5..04d66b14 100644
--- a/plugins/calendar/calendar_ui.js
+++ b/plugins/calendar/calendar_ui.js
@@ -1,4326 +1,4327 @@
/**
* Client UI Javascript for the Calendar plugin
*
* @author Lazlo Westerhof <hello@lazlo.me>
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* @licstart The following is the entire license notice for the
* JavaScript code in this file.
*
* Copyright (C) 2010, Lazlo Westerhof <hello@lazlo.me>
* Copyright (C) 2014-2015, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* @licend The above is the entire license notice
* for the JavaScript code in this file.
*/
// Roundcube calendar UI client class
function rcube_calendar_ui(settings)
{
// extend base class
rcube_calendar.call(this, settings);
/*** member vars ***/
this.is_loading = false;
this.selected_event = null;
this.selected_calendar = null;
this.search_request = null;
this.saving_lock = null;
this.calendars = {};
this.quickview_sources = [];
/*** private vars ***/
var DAY_MS = 86400000;
var HOUR_MS = 3600000;
var me = this;
var day_clicked = day_clicked_ts = 0;
var ignore_click = false;
var event_defaults = { free_busy:'busy', alarms:'' };
var event_attendees = [];
var calendars_list;
var calenders_search_list;
var calenders_search_container;
var search_calendars = {};
var attendees_list;
var resources_list;
var resources_treelist;
var resources_data = {};
var resources_index = [];
var resource_owners = {};
var resources_events_source = { url:null, editable:false };
var freebusy_ui = { workinhoursonly:false, needsupdate:false };
var freebusy_data = {};
var current_view = null;
var count_sources = [];
var event_sources = [];
var exec_deferred = 1;
var ui_loading = rcmail.set_busy(true, 'loading');
// global fullcalendar settings
var fullcalendar_defaults = {
theme: false,
aspectRatio: 1,
timezone: false, // will treat the given date strings as in local (browser's) timezone
monthNames: settings.months,
monthNamesShort: settings.months_short,
dayNames: settings.days,
dayNamesShort: settings.days_short,
weekNumbers: settings.show_weekno > 0,
weekNumberTitle: rcmail.gettext('weekshort', 'calendar') + ' ',
weekNumberCalculation: 'ISO',
firstDay: settings.first_day,
firstHour: settings.first_hour,
slotDuration: {minutes: 60/settings.timeslots},
businessHours: {
start: settings.work_start + ':00',
end: settings.work_end + ':00'
},
scrollTime: settings.work_start + ':00',
views: {
list: {
titleFormat: settings.dates_long,
listDayFormat: settings.date_long,
visibleRange: function(currentDate) {
return {
start: currentDate.clone(),
end: currentDate.clone().add(settings.agenda_range, 'days')
}
}
},
month: {
columnFormat: 'ddd', // Mon
titleFormat: 'MMMM YYYY',
eventLimit: 4
},
week: {
columnFormat: 'ddd ' + settings.date_short, // Mon 9/7
titleFormat: settings.dates_long
},
day: {
columnFormat: 'dddd ' + settings.date_short, // Monday 9/7
titleFormat: 'dddd ' + settings.date_long
}
},
timeFormat: settings.time_format,
slotLabelFormat: settings.time_format,
defaultView: rcmail.env.view || settings.default_view,
allDayText: rcmail.gettext('all-day', 'calendar'),
buttonText: {
today: settings['today'],
day: rcmail.gettext('day', 'calendar'),
week: rcmail.gettext('week', 'calendar'),
month: rcmail.gettext('month', 'calendar'),
list: rcmail.gettext('agenda', 'calendar')
},
buttonIcons: {
prev: 'left-single-arrow',
next: 'right-single-arrow'
},
nowIndicator: settings.time_indicator,
eventLimitText: function(num) {
return rcmail.gettext('andnmore', 'calendar').replace('$nr', num);
},
// event rendering
eventRender: function(event, element, view) {
if (view.name != 'list') {
element.attr('title', event.title);
}
if (view.name != 'month') {
if (view.name == 'list') {
var loc = $('<td>').attr('class', 'fc-event-location');
if (event.location)
loc.text(event.location);
element.find('.fc-list-item-title').after(loc);
}
else if (event.location) {
element.find('div.fc-title').after($('<div class="fc-event-location">').html('@ ' + Q(event.location)));
}
var time_element = element.find('div.fc-time');
if (event.recurrence)
time_element.append('<i class="fc-icon-recurring"></i>');
if (event.alarms || (event.valarms && event.valarms.length))
time_element.append('<i class="fc-icon-alarms"></i>');
}
if (event.status) {
element.addClass('cal-event-status-' + String(event.status).toLowerCase());
}
set_event_colors(element, event, view.name);
element.attr('aria-label', event.title + ', ' + me.event_date_text(event, true));
},
// callback when a specific event is clicked
eventClick: function(event, ev, view) {
if (!event.temp && (!event.className || event.className.indexOf('fc-type-freebusy') < 0))
event_show_dialog(event, ev);
}
};
/*** imports ***/
var Q = this.quote_html;
var text2html = this.text2html;
var event_date_text = this.event_date_text;
var date2unixtime = this.date2unixtime;
var fromunixtime = this.fromunixtime;
var parseISO8601 = this.parseISO8601;
var date2servertime = this.date2ISO8601;
var render_message_links = this.render_message_links;
/*** private methods ***/
// same as str.split(delimiter) but it ignores delimiters within quoted strings
var explode_quoted_string = function(str, delimiter)
{
var result = [],
strlen = str.length,
q, p, i, chr, last;
for (q = p = i = 0; i < strlen; i++) {
chr = str.charAt(i);
if (chr == '"' && last != '\\') {
q = !q;
}
else if (!q && chr == delimiter) {
result.push(str.substring(p, i));
p = i + 1;
}
last = chr;
}
result.push(str.substr(p));
return result;
};
// Change the first charcter to uppercase
var ucfirst = function(str)
{
return str.charAt(0).toUpperCase() + str.substr(1);
};
// clone the given date object and optionally adjust time
var clone_date = function(date, adjust)
{
var d = 'toDate' in date ? date.toDate() : new Date(date.getTime());
// set time to 00:00
if (adjust == 1) {
d.setHours(0);
d.setMinutes(0);
}
// set time to 23:59
else if (adjust == 2) {
d.setHours(23);
d.setMinutes(59);
}
return d;
};
// fix date if jumped over a DST change
var fix_date = function(date)
{
if (date.getHours() == 23)
date.setTime(date.getTime() + HOUR_MS);
else if (date.getHours() > 0)
date.setHours(0);
};
var date2timestring = function(date, dateonly)
{
return date2servertime(date).replace(/[^0-9]/g, '').substr(0, (dateonly ? 8 : 14));
}
var format_date = function(date, format)
{
return $.fullCalendar.formatDate('toDate' in date ? date : moment(date), format);
};
var format_datetime = function(date, mode, voice)
{
return me.format_datetime(date, mode, voice);
}
var render_link = function(url)
{
var islink = false, href = url;
if (url.match(/^[fhtpsmailo]+?:\/\//i)) {
islink = true;
}
else if (url.match(/^[a-z0-9.-:]+(\/|$)/i)) {
islink = true;
href = 'http://' + url;
}
return islink ? '<a href="' + Q(href) + '" target="_blank">' + Q(url) + '</a>' : Q(url);
}
// determine whether the given date is on a weekend
var is_weekend = function(date)
{
return date.getDay() == 0 || date.getDay() == 6;
};
var is_workinghour = function(date)
{
if (settings['work_start'] > settings['work_end'])
return date.getHours() >= settings['work_start'] || date.getHours() < settings['work_end'];
else
return date.getHours() >= settings['work_start'] && date.getHours() < settings['work_end'];
};
var set_event_colors = function(element, event, mode)
{
var bg_color = '', border_color = '',
cat = String(event.categories),
color = event.calendar && me.calendars[event.calendar] ? me.calendars[event.calendar].color : '',
cat_color = rcmail.env.calendar_categories[cat] ? rcmail.env.calendar_categories[cat] : color;
switch (settings.event_coloring) {
case 1:
bg_color = border_color = cat_color;
break;
case 2:
border_color = color;
bg_color = cat_color;
break;
case 3:
border_color = cat_color;
bg_color = color;
break;
default:
bg_color = border_color = color;
break;
}
var css = {
'border-color': border_color,
'background-color': bg_color,
'color': me.text_color(bg_color)
};
if (String(css['border-color']).match(/^#?f+$/i))
delete css['border-color'];
$.each(css, function(i, v) { if (!v) delete css[i]; else if (v.charAt(0) != '#') css[i] = '#' + v; });
if (mode == 'list') {
bg_color = css['background-color'];
if (bg_color && !bg_color.match(/^#?f+$/i))
$(element).find('.fc-event-dot').css('background-color', bg_color);
}
else
$(element).css(css);
};
var load_attachment = function(data)
{
var event = data.record,
query = {_id: data.attachment.id, _event: event.recurrence_id || event.id, _cal: event.calendar};
if (event.rev)
query._rev = event.rev;
if (event.calendar == "--invitation--itip")
$.extend(query, {_uid: event._uid, _part: event._part, _mbox: event._mbox});
libkolab.load_attachment(query, data.attachment);
};
// build event attachments list
var event_show_attachments = function(list, container, event, edit)
{
libkolab.list_attachments(list, container, edit, event,
function(id) { remove_attachment(id); },
function(data) { load_attachment(data); }
);
};
var remove_attachment = function(id)
{
rcmail.env.deleted_attachments.push(id);
};
// event details dialog (show only)
var event_show_dialog = function(event, ev, temp)
{
var $dialog = $("#eventshow");
var calendar = event.calendar && me.calendars[event.calendar] ? me.calendars[event.calendar] : { editable:false, rights:'lrs' };
if (!temp)
me.selected_event = event;
if ($dialog.is(':ui-dialog'))
$dialog.dialog('close');
// remove status-* classes
$dialog.removeClass(function(i, oldclass) {
var oldies = String(oldclass).split(' ');
return $.grep(oldies, function(cls) { return cls.indexOf('status-') === 0; }).join(' ');
});
// convert start/end dates if not done yet by fullcalendar
if (typeof event.start == 'string')
event.start = parseISO8601(event.start);
if (typeof event.end == 'string')
event.end = parseISO8601(event.end);
// allow other plugins to do actions when event form is opened
rcmail.triggerEvent('calendar-event-init', {o: event});
$dialog.find('div.event-section, div.event-line, .form-group').hide();
$('#event-title').html(Q(event.title)).show();
if (event.location)
$('#event-location').html('@ ' + text2html(event.location)).show();
if (event.description)
$('#event-description').show().find('.event-text').html(text2html(event.description, 300, 6));
if (event.vurl)
$('#event-url').show().find('.event-text').html(render_link(event.vurl));
if (event.recurrence && event.recurrence_text)
$('#event-repeat').show().find('.event-text').html(Q(event.recurrence_text));
if (event.valarms && event.alarms_text)
$('#event-alarm').show().find('.event-text').html(Q(event.alarms_text).replace(',', ',<br>'));
if (calendar.name)
$('#event-calendar').show().find('.event-text').html(Q(calendar.name)).addClass('cal-'+calendar.id);
if (event.categories && String(event.categories).length)
$('#event-category').show().find('.event-text').text(event.categories).addClass('cat-'+String(event.categories).toLowerCase().replace(rcmail.identifier_expr, ''));
if (event.free_busy)
$('#event-free-busy').show().find('.event-text').text(rcmail.gettext(event.free_busy, 'calendar'));
if (event.priority > 0) {
var priolabels = [ '', rcmail.gettext('highest'), rcmail.gettext('high'), '', '', rcmail.gettext('normal'), '', '', rcmail.gettext('low'), rcmail.gettext('lowest') ];
$('#event-priority').show().find('.event-text').text(event.priority+' '+priolabels[event.priority]);
}
if (event.status) {
var status_lc = String(event.status).toLowerCase();
$('#event-status').show().find('.event-text').text(rcmail.gettext('status-'+status_lc,'calendar'));
$('#event-status-badge > span').text(rcmail.gettext('status-'+status_lc,'calendar'));
$dialog.addClass('status-'+status_lc);
}
if (event.created || event.changed) {
var created = parseISO8601(event.created),
changed = parseISO8601(event.changed);
$('.event-created', $dialog).text(created ? format_datetime(created) : rcmail.gettext('unknown','calendar'));
$('.event-changed', $dialog).text(changed ? format_datetime(changed) : rcmail.gettext('unknown','calendar'));
$('#event-created,#event-changed,#event-created-changed').show()
}
// create attachments list
if ($.isArray(event.attachments)) {
event_show_attachments(event.attachments, $('#event-attachments').find('.event-text'), event);
if (event.attachments.length > 0) {
$('#event-attachments').show();
}
}
else if (calendar.attachments) {
// fetch attachments, some drivers doesn't set 'attachments' prop of the event?
}
// build attachments list
$('#event-links').hide();
if ($.isArray(event.links) && event.links.length) {
render_message_links(event.links || [], $('#event-links').find('.event-text'), false, 'calendar');
$('#event-links').show();
}
// list event attendees
if (calendar.attendees && event.attendees) {
// sort resources to the end
event.attendees.sort(function(a,b) {
var j = a.cutype == 'RESOURCE' ? 1 : 0,
k = b.cutype == 'RESOURCE' ? 1 : 0;
return (j - k);
});
var data, mystatus = null, rsvp, line, morelink, html = '', overflow = '',
organizer = me.is_organizer(event), num_attendees = event.attendees.length;
for (var j=0; j < num_attendees; j++) {
data = event.attendees[j];
if (data.email) {
if (data.role != 'ORGANIZER' && is_this_me(data.email)) {
mystatus = (data.status || 'UNKNOWN').toLowerCase();
if (data.status == 'NEEDS-ACTION' || data.status == 'TENTATIVE' || data.rsvp)
rsvp = mystatus;
}
}
line = rcube_libcalendaring.attendee_html(data);
if (morelink)
overflow += ' ' + line;
else
html += ' ' + line;
// stop listing attendees
if (j == 7 && num_attendees > 8) {
morelink = $('<a href="#more" class="morelink"></a>').html(rcmail.gettext('andnmore', 'calendar').replace('$nr', num_attendees - j - 1));
}
}
if (html && (num_attendees > 1 || !organizer)) {
$('#event-attendees').show()
.find('.event-text')
.html(html)
.find('a.mailtolink').click(event_attendee_click);
// display all attendees in a popup when clicking the "more" link
if (morelink) {
$('#event-attendees .event-text').append(morelink);
morelink.click(function(e){
rcmail.simple_dialog(
'<div id="all-event-attendees" class="event-attendees">' + html + overflow + '</div>',
rcmail.gettext('tabattendees','calendar'),
null,
{width: 450, cancel_button: 'close'});
$('#all-event-attendees a.mailtolink').click(event_attendee_click);
return false;
});
}
}
if (mystatus && !rsvp) {
$('#event-partstat').show().find('.changersvp, .event-text')
.removeClass('accepted tentative declined delegated needs-action unknown')
.addClass(mystatus);
$('#event-partstat').find('.event-text')
.text(rcmail.gettext('status' + mystatus, 'libcalendaring'));
}
var show_rsvp = rsvp && !organizer && event.status != 'CANCELLED' && me.has_permission(calendar, 'v');
$('#event-rsvp')[(show_rsvp ? 'show' : 'hide')]();
$('#event-rsvp .rsvp-buttons input').prop('disabled', false).filter('input[rel="'+(mystatus || '')+'"]').prop('disabled', true);
if (show_rsvp && event.comment)
$('#event-rsvp-comment').show().find('.event-text').html(Q(event.comment));
$('#event-rsvp a.reply-comment-toggle').show();
$('#event-rsvp .itip-reply-comment textarea').hide().val('');
if (event.recurrence && event.id) {
var sel = event._savemode || (event.thisandfuture ? 'future' : (event.isexception ? 'current' : 'all'));
$('#event-rsvp .rsvp-buttons').addClass('recurring');
}
else {
$('#event-rsvp .rsvp-buttons').removeClass('recurring');
}
}
var buttons = [], is_removable_event = function(event, calendar) {
// for invitation calendars check permissions of the original folder
if (event._folder_id)
calendar = me.calendars[event._folder_id];
return calendar && me.has_permission(calendar, 'td');
};
if (!temp && calendar.editable && event.editable !== false) {
buttons.push({
text: rcmail.gettext('edit', 'calendar'),
'class': 'edit mainaction',
click: function() {
event_edit_dialog('edit', event);
}
});
}
if (!temp && is_removable_event(event, calendar) && event.editable !== false) {
buttons.push({
text: rcmail.gettext('delete', 'calendar'),
'class': 'delete',
click: function() {
me.delete_event(event);
$dialog.dialog('close');
}
});
}
buttons.push({
text: rcmail.gettext('close', 'calendar'),
'class': 'cancel',
click: function() {
$dialog.dialog('close');
}
});
var close_func = function(e) {
rcmail.command('menu-close', 'eventoptionsmenu', null, e);
$('.libcal-rsvp-replymode').hide();
};
// open jquery UI dialog
$dialog.dialog({
modal: true,
resizable: true,
closeOnEscape: true,
title: me.event_date_text(event),
open: function() {
$dialog.attr('aria-hidden', 'false');
setTimeout(function(){
$dialog.parent().find('button:not(.ui-dialog-titlebar-close,.delete)').first().focus();
}, 5);
},
close: function(e) {
close_func(e);
$dialog.dialog('close');
},
dragStart: close_func,
resizeStart: close_func,
buttons: buttons,
minWidth: 320,
width: 420
}).show();
// remember opener element (to be focused on close)
$dialog.data('opener', ev && rcube_event.is_keyboard(ev) ? ev.target : null);
// set voice title on dialog widget
$dialog.dialog('widget').removeAttr('aria-labelledby')
.attr('aria-label', me.event_date_text(event, true) + ', ', event.title);
// set dialog size according to content
me.dialog_resize($dialog.get(0), $dialog.height(), 420);
// add link for "more options" drop-down
if (!temp && !event.temporary && event.calendar != '_resource') {
$('<a>')
.attr({href: '#', 'class': 'dropdown-link btn btn-link options', 'data-popup-pos': 'top'})
.text(rcmail.gettext('eventoptions','calendar'))
.click(function(e) {
return rcmail.command('menu-open','eventoptionsmenu', this, e);
})
.appendTo($dialog.parent().find('.ui-dialog-buttonset'));
}
rcmail.enable_command('event-history', calendar.history);
rcmail.triggerEvent('calendar-event-dialog', {dialog: $dialog});
};
// event handler for clicks on an attendee link
var event_attendee_click = function(e)
{
var cutype = $(this).attr('data-cutype'),
mailto = this.href.substr(7);
if (rcmail.env.calendar_resources && cutype == 'RESOURCE') {
event_resources_dialog(mailto);
}
else {
rcmail.command('compose', mailto, e ? e.target : null, e);
}
return false;
};
// bring up the event dialog (jquery-ui popup)
var event_edit_dialog = function(action, event)
{
// copy opener element from show dialog
var op_elem = $("#eventshow:ui-dialog").data('opener');
// close show dialog first
$("#eventshow:ui-dialog").data('opener', null).dialog('close');
var $dialog = $('<div>');
var calendar = event.calendar && me.calendars[event.calendar] ? me.calendars[event.calendar] : { editable:true, rights: action=='new' ? 'lrwitd' : 'lrs' };
me.selected_event = $.extend($.extend({}, event_defaults), event); // clone event object (with defaults)
event = me.selected_event; // change reference to clone
freebusy_ui.needsupdate = false;
if (rcmail.env.action == 'dialog-ui')
calendar.attendees = false; // TODO: allow Attendees tab in Save-as-event dialog
// reset dialog first
$('#eventedit form').trigger('reset');
$('#event-panel-recurrence input, #event-panel-recurrence select, #event-panel-attachments input').prop('disabled', false);
$('#event-panel-recurrence, #event-panel-attachments').removeClass('disabled');
// allow other plugins to do actions when event form is opened
rcmail.triggerEvent('calendar-event-init', {o: event});
// event details
var title = $('#edit-title').val(event.title || '');
var location = $('#edit-location').val(event.location || '');
var description = $('#edit-description').text(event.description || '');
var vurl = $('#edit-url').val(event.vurl || '');
var categories = $('#edit-categories').val(event.categories);
var calendars = $('#edit-calendar').val(event.calendar);
var eventstatus = $('#edit-event-status').val(event.status);
var freebusy = $('#edit-free-busy').val(event.free_busy);
var priority = $('#edit-priority').val(event.priority);
var syncstart = $('#edit-recurrence-syncstart input');
var end = 'toDate' in event.end ? event.end : moment(event.end);
var start = 'toDate' in event.start ? event.start : moment(event.start);
var duration = Math.round((end.format('x') - start.format('x')) / 1000);
// Correct the fullCalendar end date for all-day events
if (!end.hasTime()) {
end.subtract(1, 'days');
}
var startdate = $('#edit-startdate').val(format_date(start, settings.date_format)).data('duration', duration);
var starttime = $('#edit-starttime').val(format_date(start, settings.time_format)).show();
var enddate = $('#edit-enddate').val(format_date(end, settings.date_format));
var endtime = $('#edit-endtime').val(format_date(end, settings.time_format)).show();
var allday = $('#edit-allday').get(0);
var notify = $('#edit-attendees-donotify').get(0);
var invite = $('#edit-attendees-invite').get(0);
var comment = $('#edit-attendees-comment');
// make sure any calendar is selected
if (!calendars.val())
calendars.val($('option:first', calendars).attr('value'));
invite.checked = settings.itip_notify & 1 > 0;
notify.checked = me.has_attendees(event) && invite.checked;
if (event.allDay) {
starttime.val("12:00").hide();
endtime.val("13:00").hide();
allday.checked = true;
}
else {
allday.checked = false;
}
// set calendar selection according to permissions
calendars.find('option').each(function(i, opt) {
var cal = me.calendars[opt.value] || {};
$(opt).prop('disabled', !(cal.editable || (action == 'new' && me.has_permission(cal, 'i'))))
});
// set alarm(s)
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')]();
// check categories drop-down: add value if not exists
if (event.categories && !categories.find("option[value='"+event.categories+"']").length) {
$('<option>').attr('value', event.categories).text(event.categories).appendTo(categories).prop('selected', true);
}
if ($.isArray(event.links) && event.links.length) {
render_message_links(event.links, $('#edit-event-links .event-text'), true, 'calendar');
$('#edit-event-links').show();
}
else {
$('#edit-event-links').hide();
}
// show warning if editing a recurring event
if (event.id && event.recurrence) {
var sel = event._savemode || (event.thisandfuture ? 'future' : (event.isexception ? 'current' : 'all'));
$('#edit-recurring-warning').show();
$('input.edit-recurring-savemode[value="'+sel+'"]').prop('checked', true).change();
}
else
$('#edit-recurring-warning').hide();
// init attendees tab
var organizer = !event.attendees || me.is_organizer(event),
allow_invitations = organizer || (calendar.owner && calendar.owner == 'anonymous') || settings.invite_shared;
event_attendees = [];
attendees_list = $('#edit-attendees-table > tbody').html('');
resources_list = $('#edit-resources-table > tbody').html('');
$('#edit-attendees-notify')[(action != 'new' && allow_invitations && me.has_attendees(event) && (settings.itip_notify & 2) ? 'show' : 'hide')]();
$('#edit-localchanges-warning')[(action != 'new' && me.has_attendees(event) && !(allow_invitations || (calendar.owner && me.is_organizer(event, calendar.owner))) ? 'show' : 'hide')]();
var load_attendees_tab = function()
{
var j, data, organizer_attendee, reply_selected = 0;
if (event.attendees) {
for (j=0; j < event.attendees.length; j++) {
data = event.attendees[j];
// reset attendee status
if (event._savemode == 'new' && data.role != 'ORGANIZER') {
data.status = 'NEEDS-ACTION';
delete data.noreply;
}
add_attendee(data, !allow_invitations);
if (allow_invitations && data.role != 'ORGANIZER' && !data.noreply)
reply_selected++;
if (data.role == 'ORGANIZER')
organizer_attendee = data;
}
}
// make sure comment box is visible if at least one attendee has reply enabled
// or global "send invitations" checkbox is checked
$('#eventedit .attendees-commentbox')[(reply_selected || invite.checked ? 'show' : 'hide')]();
// select the correct organizer identity
var identity_id = 0;
$.each(settings.identities, function(i,v){
if (organizer && typeof organizer == 'object' && v == organizer.email) {
identity_id = i;
return false;
}
});
// In case the user is not the (shared) event organizer we'll add the organizer to the selection list
if (!identity_id && !organizer && organizer_attendee) {
var organizer_name = organizer_attendee.email;
if (organizer_attendee.name)
organizer_name = '"' + organizer_attendee.name + '" <' + organizer_name + '>';
$('#edit-identities-list').append($('<option value="0">').text(organizer_name));
}
$('#edit-identities-list').val(identity_id);
$('#edit-attendees-form')[(allow_invitations?'show':'hide')]();
$('#edit-attendee-schedule')[(calendar.freebusy?'show':'hide')]();
};
// attachments
var load_attachments_tab = function()
{
rcmail.enable_command('remove-attachment', 'upload-file', calendar.editable && !event.recurrence_id);
rcmail.env.deleted_attachments = [];
// we're sharing some code for uploads handling with app.js
rcmail.env.attachments = [];
rcmail.env.compose_id = event.id; // for rcmail.async_upload_form()
if ($.isArray(event.attachments)) {
event_show_attachments(event.attachments, $('#edit-attachments'), event, true);
}
else {
$('#edit-attachments > ul').empty();
// fetch attachments, some drivers doesn't set 'attachments' array for event?
}
};
// init dialog buttons
var buttons = [],
save_func = function() {
var start = allday.checked ? '12:00' : $.trim(starttime.val()),
end = allday.checked ? '13:00' : $.trim(endtime.val()),
re = /^((0?[0-9])|(1[0-9])|(2[0-3])):([0-5][0-9])(\s*[ap]\.?m\.?)?$/i;
if (!re.test(start) || !re.test(end)) {
rcmail.alert_dialog(rcmail.gettext('invalideventdates', 'calendar'));
return false;
}
start = me.parse_datetime(start, startdate.val());
end = me.parse_datetime(end, enddate.val());
if (!title.val()) {
rcmail.alert_dialog(rcmail.gettext('emptyeventtitle', 'calendar'));
return false;
}
if (start.getTime() > end.getTime()) {
rcmail.alert_dialog(rcmail.gettext('invalideventdates', 'calendar'));
return false;
}
// post data to server
var data = {
calendar: event.calendar,
start: date2servertime(start),
end: date2servertime(end),
allDay: allday.checked?1:0,
title: title.val(),
description: description.val(),
location: location.val(),
categories: categories.val(),
vurl: vurl.val(),
free_busy: freebusy.val(),
priority: priority.val(),
status: eventstatus.val(),
recurrence: me.serialize_recurrence(endtime.val()),
valarms: me.serialize_alarms('#edit-alarms'),
attendees: event_attendees,
links: me.selected_event.links,
deleted_attachments: rcmail.env.deleted_attachments,
attachments: []
};
// uploaded attachments list
for (var i in rcmail.env.attachments)
if (i.match(/^rcmfile(.+)/))
data.attachments.push(RegExp.$1);
if (organizer)
data._identity = $('#edit-identities-list option:selected').val();
// per-attendee notification suppression
var need_invitation = false;
if (allow_invitations) {
$.each(data.attendees, function (i, v) {
if (v.role != 'ORGANIZER') {
if ($('input.edit-attendee-reply[value="' + v.email + '"]').prop('checked') || v.cutype == 'RESOURCE') {
need_invitation = true;
delete data.attendees[i]['noreply'];
}
else if (settings.itip_notify > 0) {
data.attendees[i].noreply = 1;
}
}
});
}
// tell server to send notifications
if ((data.attendees.length || (event.id && event.attendees.length)) && allow_invitations && (notify.checked || invite.checked || need_invitation)) {
data._notify = settings.itip_notify;
data._comment = comment.val();
}
data.calendar = calendars.val();
if (event.id) {
data.id = event.id;
if (event.recurrence)
data._savemode = $('input.edit-recurring-savemode:checked').val();
if (data.calendar && data.calendar != event.calendar)
data._fromcalendar = event.calendar;
}
if (data.recurrence && syncstart.is(':checked'))
data.syncstart = 1;
update_event(action, data);
if (rcmail.env.action != 'dialog-ui')
$dialog.dialog("close");
};
rcmail.env.event_save_func = save_func;
// save action
buttons.push({
text: rcmail.gettext('save', 'calendar'),
'class': 'save mainaction',
click: save_func
});
buttons.push({
text: rcmail.gettext('cancel', 'calendar'),
'class': 'cancel',
click: function() {
$dialog.dialog("close");
}
});
// show/hide tabs according to calendar's feature support and activate first tab (Larry)
$('#edit-tab-attendees')[(calendar.attendees?'show':'hide')]();
$('#edit-tab-resources')[(rcmail.env.calendar_resources?'show':'hide')]();
$('#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)
$('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');
// 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');
var editform = $("#eventedit");
if (rcmail.env.action != 'dialog-ui') {
// open jquery UI dialog
$dialog.dialog({
modal: true,
resizable: true,
closeOnEscape: false,
title: rcmail.gettext((action == 'edit' ? 'edit_event' : 'new_event'), 'calendar'),
open: function() {
editform.attr('aria-hidden', 'false');
},
close: function() {
editform.hide().attr('aria-hidden', 'true').appendTo(document.body);
$dialog.dialog("destroy").remove();
rcmail.ksearch_blur();
freebusy_data = {};
rcmail.env.comm_path = comm_path_before; // restore comm_path
if (op_elem)
$(op_elem).focus();
},
buttons: buttons,
minWidth: 500,
width: 600
}).append(editform.show()); // adding form content AFTERWARDS massively speeds up opening on IE6
// set dialog size according to form content
me.dialog_resize($dialog.get(0), editform.height() + (bw.ie ? 20 : 0), 550);
rcmail.triggerEvent('calendar-event-dialog', {dialog: $dialog});
}
// init other tabs asynchronously
window.setTimeout(function(){ me.set_recurrence_edit(event); }, exec_deferred);
if (calendar.attendees)
window.setTimeout(load_attendees_tab, exec_deferred);
if (calendar.attachments)
window.setTimeout(load_attachments_tab, exec_deferred);
title.select();
};
// show event changelog in a dialog
var event_history_dialog = function(event)
{
if (!event.id || !window.libkolab_audittrail)
return false
// render dialog
var $dialog = libkolab_audittrail.object_history_dialog({
module: 'calendar',
container: '#eventhistory',
title: rcmail.gettext('objectchangelog','calendar') + ' - ' + event.title + ', ' + me.event_date_text(event),
// callback function for list actions
listfunc: function(action, rev) {
me.loading_lock = rcmail.set_busy(true, 'loading', me.loading_lock);
rcmail.http_post('event', { action:action, e:{ id:event.id, calendar:event.calendar, rev: rev } }, me.loading_lock);
},
// callback function for comparing two object revisions
comparefunc: function(rev1, rev2) {
me.loading_lock = rcmail.set_busy(true, 'loading', me.loading_lock);
rcmail.http_post('event', { action:'diff', e:{ id:event.id, calendar:event.calendar, rev1: rev1, rev2: rev2 } }, me.loading_lock);
}
});
$dialog.data('event', event);
// fetch changelog data
me.loading_lock = rcmail.set_busy(true, 'loading', me.loading_lock);
rcmail.http_post('event', { action:'changelog', e:{ id:event.id, calendar:event.calendar } }, me.loading_lock);
};
// callback from server with changelog data
var render_event_changelog = function(data)
{
var $dialog = $('#eventhistory'),
event = $dialog.data('event');
if (data === false || !data.length || !event) {
// display 'unavailable' message
$('<div class="notfound-message dialog-message warning">' + rcmail.gettext('objectchangelognotavailable','calendar') + '</div>')
.insertBefore($dialog.find('.changelog-table').hide());
return;
}
data.module = 'calendar';
libkolab_audittrail.render_changelog(data, event, me.calendars[event.calendar]);
// set dialog size according to content
me.dialog_resize($dialog.get(0), $dialog.height(), 600);
};
// callback from server with event diff data
var event_show_diff = function(data)
{
var event = me.selected_event,
$dialog = $("#eventdiff");
$dialog.find('div.event-section, div.event-line, h1.event-title-new').hide().data('set', false).find('.index').html('');
$dialog.find('div.event-section.clone, div.event-line.clone').remove();
// always show event title and date
$('.event-title', $dialog).text(event.title).removeClass('event-text-old').show();
$('.event-date', $dialog).text(me.event_date_text(event)).show();
// show each property change
$.each(data.changes, function(i,change) {
var prop = change.property, r2, html = false,
row = $('div.event-' + prop, $dialog).first();
// special case: title
if (prop == 'title') {
$('.event-title', $dialog).addClass('event-text-old').text(change['old'] || '--');
$('.event-title-new', $dialog).text(change['new'] || '--').show();
}
// no display container for this property
if (!row.length) {
return true;
}
// clone row if already exists
if (row.data('set')) {
r2 = row.clone().addClass('clone').insertAfter(row);
row = r2;
}
// format dates
if (['start','end','changed'].indexOf(prop) >= 0) {
if (change['old']) change.old_ = me.format_datetime(parseISO8601(change['old']));
if (change['new']) change.new_ = me.format_datetime(parseISO8601(change['new']));
}
// render description text
else if (prop == 'description') {
// TODO: show real text diff
if (!change.diff_ && change['old']) change.old_ = text2html(change['old']);
if (!change.diff_ && change['new']) change.new_ = text2html(change['new']);
html = true;
}
// format attendees struct
else if (prop == 'attendees') {
if (change['old']) change.old_ = rcube_libcalendaring.attendee_html(change['old']);
if (change['new']) change.new_ = rcube_libcalendaring.attendee_html($.extend({}, change['old'] || {}, change['new']));
html = true;
}
// localize priority values
else if (prop == 'priority') {
var priolabels = [ '', rcmail.gettext('highest'), rcmail.gettext('high'), '', '', rcmail.gettext('normal'), '', '', rcmail.gettext('low'), rcmail.gettext('lowest') ];
if (change['old']) change.old_ = change['old'] + ' ' + (priolabels[change['old']] || '');
if (change['new']) change.new_ = change['new'] + ' ' + (priolabels[change['new']] || '');
}
// localize status
else if (prop == 'status') {
var status_lc = String(event.status).toLowerCase();
if (change['old']) change.old_ = rcmail.gettext(String(change['old']).toLowerCase(), 'calendar');
if (change['new']) change.new_ = rcmail.gettext(String(change['new']).toLowerCase(), 'calendar');
}
// format attachments struct
if (prop == 'attachments') {
if (change['old']) event_show_attachments([change['old']], row.children('.event-text-old'), event, false);
else row.children('.event-text-old').text('--');
if (change['new']) event_show_attachments([$.extend({}, change['old'] || {}, change['new'])], row.children('.event-text-new'), event, false);
else row.children('.event-text-new').text('--');
// remove click handler as we're currentyl not able to display the according attachment contents
$('.attachmentslist li a', row).unbind('click').removeAttr('href');
}
else if (change.diff_) {
row.children('.event-text-diff').html(change.diff_);
row.children('.event-text-old, .event-text-new').hide();
}
else {
if (!html) {
// escape HTML characters
change.old_ = Q(change.old_ || change['old'] || '--')
change.new_ = Q(change.new_ || change['new'] || '--')
}
row.children('.event-text-old').html(change.old_ || change['old'] || '--');
row.children('.event-text-new').html(change.new_ || change['new'] || '--');
}
// display index number
if (typeof change.index != 'undefined') {
row.find('.index').html('(' + change.index + ')');
}
row.show().data('set', true);
// hide event-date line
if (prop == 'start' || prop == 'end')
$('.event-date', $dialog).hide();
});
var buttons = [{
text: rcmail.gettext('close', 'calendar'),
'class': 'cancel',
click: function() { $dialog.dialog('close'); }
}];
// open jquery UI dialog
$dialog.dialog({
modal: false,
resizable: true,
closeOnEscape: true,
title: rcmail.gettext('objectdiff','calendar').replace('$rev1', data.rev1).replace('$rev2', data.rev2) + ' - ' + event.title,
open: function() {
$dialog.attr('aria-hidden', 'false');
setTimeout(function(){
$dialog.parent().find('button:not(.ui-dialog-titlebar-close)').first().focus();
}, 5);
},
close: function() {
$dialog.dialog('destroy').attr('aria-hidden', 'true').hide();
},
buttons: buttons,
minWidth: 320,
width: 450
}).show();
// set dialog size according to content
me.dialog_resize($dialog.get(0), $dialog.height(), 400);
};
// close the event history dialog
var close_history_dialog = function()
{
$('#eventhistory, #eventdiff').each(function(i, elem) {
var $dialog = $(elem);
if ($dialog.is(':ui-dialog'))
$dialog.dialog('close');
});
}
// exports
this.event_show_diff = event_show_diff;
this.event_show_dialog = event_show_dialog;
this.event_history_dialog = event_history_dialog;
this.render_event_changelog = render_event_changelog;
this.close_history_dialog = close_history_dialog;
// open a dialog to display detailed free-busy information and to find free slots
var event_freebusy_dialog = function()
{
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);
var start = 'toDate' in event.start ? event.start : moment(event.start);
var duration = Math.round((end.format('x') - start.format('x')) / 1000);
freebusy_ui.startdate = $('#schedule-startdate').val(format_date(start, settings.date_format)).data('duration', duration);
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();
event.allDay = true;
}
// render time slots
var now = new Date(), fb_start = new Date(), fb_end = new Date();
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));
freebusy_ui.interval = allday.checked ? 1440 : (60 / (settings.timeslots || 1));
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 = '';
for (var i=0; i < event_attendees.length; i++) {
data = event_attendees[i];
dispname = Q(data.name || data.email);
domid = String(data.email).replace(rcmail.identifier_expr, '');
role_html = '<a class="attendee-role-toggle" id="rcmlia' + domid + '" title="' + Q(rcmail.gettext('togglerole', 'calendar')) + '"> </a>';
list_html += '<div class="attendee ' + String(data.role).toLowerCase() + '" id="rcmli' + domid + '">' + role_html + dispname + '</div>';
// clone attendees data for local modifications
freebusy_ui.attendees[i] = freebusy_ui.attendees[domid] = $.extend({}, data);
}
// add total row
list_html += '<div class="attendee spacer"> </div>';
list_html += '<div class="attendee total">' + rcmail.gettext('reqallattendees','calendar') + '</div>';
$('#schedule-attendees-list').html(list_html)
.unbind('click.roleicons')
.bind('click.roleicons', function(e) {
// toggle attendee status upon click on icon
if (e.target.id && e.target.id.match(/rcmlia(.+)/)) {
var attendee, domid = RegExp.$1,
roles = [ 'REQ-PARTICIPANT', 'OPT-PARTICIPANT', 'NON-PARTICIPANT', 'CHAIR' ];
if ((attendee = freebusy_ui.attendees[domid]) && attendee.role != 'ORGANIZER') {
var req = attendee.role != 'OPT-PARTICIPANT' && attendee.role != 'NON-PARTICIPANT';
var j = $.inArray(attendee.role, roles);
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();
update_freebusy_display(attendee.email);
}
}
}
return false;
});
// enable/disable buttons
$('#schedule-find-prev').prop('disabled', fb_start.getTime() < now.getTime());
// dialog buttons
var buttons = [
{
text: rcmail.gettext('select', 'calendar'),
'class': 'save mainaction',
click: 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());
// write role changes back to main dialog
for (var domid in freebusy_ui.attendees) {
var attendee = freebusy_ui.attendees[domid],
event_attendee = event_attendees.find(function(item) { return item.email == attendee.email});
if (event_attendee && attendee.role != event_attendee.role) {
event_attendee.role = attendee.role;
$('select.edit-attendee-role').filter(function(i,elem) { return $(elem).data('email') == attendee.email; }).val(attendee.role);
}
}
if (freebusy_ui.needsupdate)
update_freebusy_status(me.selected_event);
freebusy_ui.needsupdate = false;
$dialog.dialog("close");
}
},
{
text: rcmail.gettext('cancel', 'calendar'),
'class': 'cancel',
click: function() { $dialog.dialog("close"); }
}
];
$dialog.dialog({
modal: true,
resizable: true,
closeOnEscape: true,
title: rcmail.gettext('scheduletime', 'calendar'),
open: function() {
rcmail.ksearch_blur();
$dialog.attr('aria-hidden', 'false').find('#schedule-find-next, #schedule-find-prev').not(':disabled').first().focus();
},
close: function() {
$dialog.dialog("destroy").attr('aria-hidden', 'true').hide();
// TODO: focus opener button
},
resizeStop: function() {
render_freebusy_overlay();
},
buttons: buttons,
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);
};
// render an HTML table showing free-busy status for all the event attendees
var render_freebusy_grid = function(delta)
{
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,
curdate = new Date(),
allday = (freebusy_ui.interval == 1440),
interval = allday ? 1440 : (freebusy_ui.interval * (settings.timeslots || 1));
times_css = (allday ? 'allday ' : ''),
dates_row = '<tr class="dates">',
times_row = '<tr class="times">',
slots_row = '';
for (var s = 0, t = freebusy_ui.start.getTime(); t < freebusy_ui.end.getTime(); s++) {
curdate.setTime(t);
datestr = format_date(curdate, date_format);
if (datestr != lastdate) {
if (lastdate && !allday) break;
dates_row += '<th colspan="' + dayslots + '" class="boxtitle date' + format_date(curdate, 'DDMMYYYY') + '">' + Q(datestr) + '</th>';
lastdate = datestr;
}
// set css class according to working hours
css = is_weekend(curdate) || (freebusy_ui.interval <= 60 && !is_workinghour(curdate)) ? 'offhours' : 'workinghours';
times_row += '<td class="' + times_css + css + '" id="t-' + Math.floor(t/1000) + '">' + Q(allday ? rcmail.gettext('all-day','calendar') : format_date(curdate, settings.time_format)) + '</td>';
slots_row += '<td class="' + css + '"> </td>';
t += interval * 60000;
}
dates_row += '</tr>';
times_row += '</tr>';
// render list of attendees
var domid, data, list_html = '', times_html = '';
for (var i=0; i < event_attendees.length; i++) {
data = event_attendees[i];
domid = String(data.email).replace(rcmail.identifier_expr, '');
times_html += '<tr id="fbrow' + domid + '" class="fbcontent">' + slots_row + '</tr>';
}
// add line for all/required attendees
times_html += '<tr class="spacer"><td colspan="' + (dayslots * freebusy_ui.numdays) + '"></td>';
times_html += '<tr id="fbrowall" class="fbcontent">' + slots_row + '</tr>';
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;
table.children('thead').click(function(e){
// move event to the clicked date/time
if (e.target.id && e.target.id.match(/t-(\d+)/)) {
var newstart = new Date(RegExp.$1 * 1000);
// set time to 00:00
if (me.selected_event.allDay) {
newstart.setMinutes(0);
newstart.setHours(0);
}
update_freebusy_dates(newstart, new Date(newstart.getTime() + freebusy_ui.startdate.data('duration') * 1000));
render_freebusy_overlay();
}
});
}
// 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) {
load_freebusy_data(freebusy_ui.start, freebusy_ui.interval);
}
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'),
event_start = 'toDate' in me.selected_event.start ? me.selected_event.start.toDate() : me.selected_event.start;
event_end = 'toDate' in me.selected_event.end ? me.selected_event.end.toDate() : me.selected_event.end;
if (event_end.getTime() <= freebusy_ui.start.getTime() || event_start.getTime() >= freebusy_ui.end.getTime()) {
overlay.hide();
if (overlay.data('isdraggable'))
overlay.draggable('disable');
}
else {
var i, n, table = $('#schedule-freebusy-times'),
width = 0,
pos = { top:table.children('thead').height(), left:0 },
eventstart = date2unixtime(clone_date(me.selected_event.start, me.selected_event.allDay?1:0)),
eventend = date2unixtime(clone_date(me.selected_event.end, me.selected_event.allDay?2:0)) - 60,
slotstart = date2unixtime(freebusy_ui.start),
slotsize = freebusy_ui.interval * 60,
slotnum = freebusy_ui.interval > 60 ? 1 : (60 / freebusy_ui.interval),
cells = table.children('thead').find('td'),
cell_width = cells.first().get(0).offsetWidth,
h_margin = table.parents('table').data('h-margin'),
v_margin = table.parents('table').data('v-margin'),
slotend;
if (h_margin === undefined)
h_margin = 4;
if (v_margin === undefined)
v_margin = 4;
// iterate through slots to determine position and size of the overlay
for (i=0; i < cells.length; i++) {
for (n=0; n < slotnum; n++) {
slotend = slotstart + slotsize - 1;
// event starts in this slot: compute left
if (eventstart >= slotstart && eventstart <= slotend) {
pos.left = Math.round(i * cell_width + (cell_width / slotnum) * n);
}
// event ends in this slot: compute width
if (eventend >= slotstart && eventend <= slotend) {
width = Math.round(i * cell_width + (cell_width / slotnum) * (n + 1)) - pos.left;
}
slotstart += slotsize;
}
}
if (!width)
width = table.width() - pos.left;
// overlay is visible
if (width > 0) {
overlay.css({
width: (width - h_margin) + 'px',
height: (table.children('tbody').height() - v_margin) + 'px',
left: pos.left + 'px',
top: pos.top + 'px'
}).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);
// snap to day boundaries
if (me.selected_event.allDay) {
if (newstart.getHours() >= 12) // snap to next day
newstart.setTime(newstart.getTime() + DAY_MS);
newstart.setMinutes(0);
newstart.setHours(0);
}
else {
// round to 5 minutes
// @TODO: round to timeslots?
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 and display
update_freebusy_dates(newstart, new Date(newstart.getTime() + freebusy_ui.startdate.data('duration') * 1000));
if (me.selected_event.allDay)
render_freebusy_overlay();
}
}).data('isdraggable', true);
}
else
overlay.draggable('enable');
}
else
overlay.draggable('disable').hide();
}
};
// fetch free-busy information for each attendee from server
var load_freebusy_data = function(from, interval)
{
var start = new Date(from.getTime() - DAY_MS * 2); // start 2 days before event
fix_date(start);
var end = new Date(start.getTime() + DAY_MS * Math.max(14, freebusy_ui.numdays + 7)); // load min. 14 days
freebusy_ui.numrequired = 0;
freebusy_data.all = [];
freebusy_data.required = [];
// load free-busy information for every attendee
var domid, email;
for (var i=0; i < event_attendees.length; i++) {
if ((email = event_attendees[i].email)) {
domid = String(email).replace(rcmail.identifier_expr, '');
$('#rcmli' + domid).addClass('loading');
freebusy_ui.loading++;
$.ajax({
type: 'GET',
dataType: 'json',
url: rcmail.url('freebusy-times'),
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++) {
if (freebusy_ui.attendees[i].email == data.email) {
attendee = freebusy_ui.attendees[i];
break;
}
}
// copy data to member var
var ts, status,
req = attendee.role != 'OPT-PARTICIPANT',
start = parseISO8601(data.start);
freebusy_data.start = new Date(start);
freebusy_data.end = parseISO8601(data.end);
freebusy_data.interval = data.interval;
freebusy_data[data.email] = {};
for (i=0; i < data.slots.length; i++) {
ts = date2timestring(start, data.interval > 60);
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]++;
}
// 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]++;
}
}
};
// update free-busy grid with status loaded from server
var update_freebusy_display = function(email)
{
var status_classes = ['unknown','free','busy','tentative','out-of-office'];
var domid = String(email).replace(rcmail.identifier_expr, '');
var row = $('#fbrow' + domid);
var rowall = $('#fbrowall').children();
var dateonly = freebusy_ui.interval > 60,
t, ts = date2timestring(freebusy_ui.start, dateonly),
curdate = new Date(),
fbdata = freebusy_data[email];
if (fbdata && fbdata[ts] !== undefined && row.length) {
t = freebusy_ui.start.getTime();
row.children().each(function(i, cell) {
var j, n, attr, last, all_slots = [], slots = [],
all_cell = rowall.get(i),
cnt = dateonly ? 1 : (60 / freebusy_ui.interval),
percent = (100 / cnt);
for (n=0; n < cnt; n++) {
curdate.setTime(t);
ts = date2timestring(curdate, dateonly);
attr = {
'style': 'float:left; width:' + percent.toFixed(2) + '%',
'class': fbdata[ts] ? status_classes[fbdata[ts]] : 'unknown'
};
slots.push($('<div>').attr(attr));
// also update total row if all data was loaded
if (!freebusy_ui.loading && freebusy_data.all[ts] && all_cell) {
var w, all_status = freebusy_data.all[ts][2] ? 'busy' : 'unknown',
req_status = freebusy_data.required[ts][2] ? 'busy' : 'free';
for (j=1; j < status_classes.length; j++) {
if (freebusy_ui.numrequired && freebusy_data.required[ts][j] >= freebusy_ui.numrequired)
req_status = status_classes[j];
if (freebusy_data.all[ts][j] == event_attendees.length)
all_status = status_classes[j];
}
attr['class'] = req_status + ' all-' + all_status;
// these elements use some specific styling, so we want to minimize their number
if (last && last.attr('class').startsWith(attr['class'])) {
w = percent + parseFloat(last.css('width').replace('%', ''));
last.css('width', w.toFixed(2) + '%').attr('class', attr['class'] + ' w' + w.toFixed(0));
}
else {
last = $('<div>').attr(attr).addClass('w' + percent.toFixed(0)).append('<span>');
all_slots.push(last);
}
}
t += freebusy_ui.interval * 60000;
}
$(cell).html('').append(slots);
if (all_slots.length)
$(all_cell).html('').append(all_slots);
});
}
};
// write changed event date/times back to form fields
var update_freebusy_dates = function(start, end)
{
// fix all-day evebt times
if (me.selected_event.allDay) {
var numdays = Math.floor((me.selected_event.end.getTime() - me.selected_event.start.getTime()) / DAY_MS);
start.setHours(12);
start.setMinutes(0);
end.setTime(start.getTime() + numdays * DAY_MS);
end.setHours(13);
end.setMinutes(0);
}
me.selected_event.start = start;
me.selected_event.end = end;
freebusy_ui.startdate.val(format_date(start, settings.date_format));
freebusy_ui.starttime.val(format_date(start, settings.time_format));
freebusy_ui.enddate.val(format_date(end, settings.date_format));
freebusy_ui.endtime.val(format_date(end, settings.time_format));
freebusy_ui.needsupdate = true;
};
// attempt to find a time slot where all attemdees are available
var freebusy_find_slot = function(dir)
{
// exit if free-busy data isn't available yet
if (!freebusy_data || !freebusy_data.start)
return false;
var event = me.selected_event,
eventstart = clone_date(event.start, event.allDay ? 1 : 0).getTime(), // calculate with integers
eventend = clone_date(event.end, event.allDay ? 2 : 0).getTime(),
duration = eventend - eventstart - (event.allDay ? HOUR_MS : 0), /* make sure we don't cross day borders on DST change */
sinterval = freebusy_data.interval * 60000,
intvlslots = 1,
numslots = Math.ceil(duration / sinterval),
fb_start = freebusy_data.start.getTime(),
fb_end = freebusy_data.end.getTime(),
checkdate, slotend, email, ts, slot, slotdate = new Date(),
candidatecount = 0, candidatestart = false, success = false;
// shift event times to next possible slot
eventstart += sinterval * intvlslots * dir;
eventend += sinterval * intvlslots * dir;
// iterate through free-busy slots and find candidates
for (slot = dir > 0 ? fb_start : fb_end - sinterval;
(dir > 0 && slot < fb_end) || (dir < 0 && slot >= fb_start);
slot += sinterval * dir
) {
slotdate.setTime(slot);
// fix slot if just crossed a DST change
if (event.allDay) {
fix_date(slotdate);
slot = slotdate.getTime();
}
slotend = slot + sinterval;
if ((dir > 0 && slotend <= eventstart) || (dir < 0 && slot >= eventend)) // skip
continue;
// respect workinghours setting
if (freebusy_ui.workinhoursonly) {
if (is_weekend(slotdate) || (freebusy_data.interval <= 60 && !is_workinghour(slotdate))) { // skip off-hours
candidatestart = false;
candidatecount = 0;
continue;
}
}
if (!candidatestart)
candidatestart = slot;
ts = date2timestring(slotdate, freebusy_data.interval > 60);
// check freebusy data for all attendees
for (var i=0; i < event_attendees.length; i++) {
if (freebusy_ui.attendees[i].role != 'OPT-PARTICIPANT' && (email = freebusy_ui.attendees[i].email) && freebusy_data[email] && freebusy_data[email][ts] > 1) {
candidatestart = false;
break;
}
}
// occupied slot
if (!candidatestart) {
slot += Math.max(0, intvlslots - candidatecount - 1) * sinterval * dir;
candidatecount = 0;
continue;
}
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);
'toDate' in event.end ? (event.end = new Date(candidatestart + duration)) : event.end.setTime(candidatestart + duration);
success = true;
break;
}
}
// update event date/time display
if (success) {
update_freebusy_dates(event.start, event.end);
// move freebusy grid if necessary
var event_start = 'toDate' in event.start ? event.start.toDate() : event.start,
event_end = 'toDate' in event.end ? event.end.toDate() : event.end,
offset = Math.ceil((event_start.getTime() - freebusy_ui.end.getTime()) / DAY_MS),
now = new Date();
if (event_start.getTime() >= freebusy_ui.end.getTime())
render_freebusy_grid(Math.max(1, offset));
else if (event_end.getTime() <= freebusy_ui.start.getTime())
render_freebusy_grid(Math.min(-1, offset));
else
render_freebusy_overlay();
$('#schedule-find-prev').prop('disabled', event_start.getTime() < now.getTime());
// speak new selection
rcmail.display_message(rcmail.gettext('suggestedslot', 'calendar') + ': ' + me.event_date_text(event, true), 'voice');
}
else {
rcmail.alert_dialog(rcmail.gettext('noslotfound','calendar'));
}
};
// update event properties and attendees availability if event times have changed
var event_times_changed = function()
{
if (me.selected_event) {
var allday = $('#edit-allday').get(0);
me.selected_event.allDay = allday.checked;
me.selected_event.start = me.parse_datetime(allday.checked ? '12:00' : $('#edit-starttime').val(), $('#edit-startdate').val());
me.selected_event.end = me.parse_datetime(allday.checked ? '13:00' : $('#edit-endtime').val(), $('#edit-enddate').val());
if (event_attendees)
freebusy_ui.needsupdate = true;
$('#edit-startdate').data('duration', Math.round((me.selected_event.end.getTime() - me.selected_event.start.getTime()) / 1000));
}
};
// add the given list of participants
var add_attendees = function(names, params)
{
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 = '';
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($.extend({ email:email, name:name }, params));
success = true;
}
else {
rcmail.alert_dialog(rcmail.gettext('noemailwarning'));
}
}
return success;
};
// add the given attendee to the list
var add_attendee = function(data, readonly, before)
{
if (!me.selected_event)
return false;
// check for dupes...
var exists = false;
$.each(event_attendees, function(i, v){ exists |= (v.email == data.email); });
if (exists)
return false;
var calendar = me.selected_event && me.calendars[me.selected_event.calendar] ? me.calendars[me.selected_event.calendar] : me.calendars[me.selected_calendar];
var dispname = Q(data.name || data.email);
dispname = '<span' + ((data.email && data.email != dispname) ? ' title="' + Q(data.email) + '"' : '') + '>' + dispname + '</span>';
// role selection
var organizer = data.role == 'ORGANIZER';
var opts = {};
if (organizer)
opts.ORGANIZER = rcmail.gettext('calendar.roleorganizer');
opts['REQ-PARTICIPANT'] = rcmail.gettext('calendar.rolerequired');
opts['OPT-PARTICIPANT'] = rcmail.gettext('calendar.roleoptional');
opts['NON-PARTICIPANT'] = rcmail.gettext('calendar.rolenonparticipant');
if (data.cutype != 'RESOURCE')
opts['CHAIR'] = rcmail.gettext('calendar.rolechair');
if (organizer && !readonly)
dispname = rcmail.env['identities-selector'];
var select = '<select class="edit-attendee-role form-control custom-select"'
+ ' data-email="' + Q(data.email) + '"'
+ (organizer || readonly ? ' disabled="true"' : '')
+ ' aria-label="' + rcmail.gettext('role','calendar') + '">';
for (var r in opts)
select += '<option value="'+ r +'" class="' + r.toLowerCase() + '"' + (data.role == r ? ' selected="selected"' : '') +'>' + Q(opts[r]) + '</option>';
select += '</select>';
// delete icon
var icon = rcmail.env.deleteicon ? '<img src="' + rcmail.env.deleteicon + '" alt="" />' : '<span class="inner">' + Q(rcmail.gettext('delete')) + '</span>';
var dellink = '<a href="#delete" class="iconlink icon button delete deletelink" title="' + Q(rcmail.gettext('delete')) + '">' + icon + '</a>';
var tooltip = '', status = (data.status || '').toLowerCase(),
status_label = rcmail.gettext('status' + status, 'libcalendaring');
// send invitation checkbox
var invbox = '<input type="checkbox" class="edit-attendee-reply" value="' + Q(data.email) +'" title="' + Q(rcmail.gettext('calendar.sendinvitations')) + '" '
+ (!data.noreply && settings.itip_notify & 1 ? 'checked="checked" ' : '') + '/>';
if (data['delegated-to'])
tooltip = rcmail.gettext('libcalendaring.delegatedto') + ' ' + data['delegated-to'];
else if (data['delegated-from'])
tooltip = rcmail.gettext('libcalendaring.delegatedfrom') + ' ' + data['delegated-from'];
else if (!status && organizer)
tooltip = rcmail.gettext('statusorganizer', 'libcalendaring');
else if (status)
tooltip = status_label;
// add expand button for groups
if (data.cutype == 'GROUP') {
dispname += ' <a href="#expand" data-email="' + Q(data.email) + '" class="iconbutton add expandlink" title="' + rcmail.gettext('expandattendeegroup','libcalendaring') + '">' +
rcmail.gettext('expandattendeegroup','libcalendaring') + '</a>';
}
var avail = data.email ? 'loading' : 'unknown';
var table = rcmail.env.calendar_resources && data.cutype == 'RESOURCE' ? resources_list : attendees_list;
var img_src = rcmail.assets_path('program/resources/blank.gif');
var elastic = $(table).parents('.no-img').length > 0;
var avail_tag = elastic ? ('<span class="' + avail + '"') : ('<img alt="" src="' + img_src + '" class="availabilityicon ' + avail + '"');
var html = '<td class="role">' + select + '</td>' +
'<td class="name"><span class="attendee-name">' + dispname + '</span></td>' +
'<td class="availability">' + avail_tag + ' data-email="' + data.email + '" /></td>' +
'<td class="confirmstate"><span class="attendee ' + (status || 'organizer') + '" title="' + Q(tooltip) + '">' + Q(status && !elastic ? status_label : '') + '</span></td>' +
(data.cutype != 'RESOURCE' ? '<td class="invite">' + (organizer || readonly || !invbox ? '' : invbox) + '</td>' : '') +
'<td class="options">' + (organizer || readonly ? '' : dellink) + '</td>';
var tr = $('<tr>')
.addClass(String(data.role).toLowerCase())
.html(html);
if (before)
tr.insertBefore(before)
else
tr.appendTo(table);
tr.find('a.deletelink').click({ id:(data.email || data.name) }, function(e) { remove_attendee(this, e.data.id); return false; });
tr.find('a.expandlink').click(data, function(e) { me.expand_attendee_group(e, add_attendee, remove_attendee); return false; });
tr.find('input.edit-attendee-reply').click(function() {
var enabled = $('#edit-attendees-invite:checked').length || $('input.edit-attendee-reply:checked').length;
$('#eventedit .attendees-commentbox')[enabled ? 'show' : 'hide']();
});
tr.find('select.edit-attendee-role').change(function() { data.role = $(this).val(); });
// select organizer identity
if (data.identity_id)
$('#edit-identities-list').val(data.identity_id);
// check free-busy status
if (avail == 'loading') {
check_freebusy_status(tr.find('.availability > *:first'), data.email, me.selected_event);
}
// Make Elastic checkboxes pretty
if (window.UI && UI.pretty_checkbox) {
$(tr).find('input[type=checkbox]').each(function() { UI.pretty_checkbox(this); });
$(tr).find('select').each(function() { UI.pretty_select(this); });
}
event_attendees.push(data);
return true;
};
// iterate over all attendees and update their free-busy status display
var update_freebusy_status = function(event)
{
attendees_list.find('.availability > *').each(function(i,v) {
var email, icon = $(this);
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)
{
var calendar = event.calendar && me.calendars[event.calendar] ? me.calendars[event.calendar] : { freebusy:false };
if (!calendar.freebusy) {
$(icon).attr('class', 'availabilityicon unknown');
return;
}
icon = $(icon).attr('class', 'availabilityicon loading');
$.ajax({
type: 'GET',
dataType: 'html',
url: rcmail.url('freebusy-status'),
data: { email:email, start:date2servertime(clone_date(event.start, event.allDay?1:0)), end:date2servertime(clone_date(event.end, event.allDay?2:0)), _remote: 1 },
success: function(status){
var avail = String(status).toLowerCase();
icon.removeClass('loading').addClass(avail).attr('title', rcmail.gettext('avail' + avail, 'calendar'));
},
error: function(){
icon.removeClass('loading').addClass('unknown').attr('title', rcmail.gettext('availunknown', 'calendar'));
}
});
};
// remove an attendee from the list
var remove_attendee = function(elem, id)
{
$(elem).closest('tr').remove();
event_attendees = $.grep(event_attendees, function(data){ return (data.name != id && data.email != id) });
};
// open a dialog to display detailed free-busy information and to find free slots
var event_resources_dialog = function(search)
{
var $dialog = $('#eventresourcesdialog');
if ($dialog.is(':ui-dialog'))
$dialog.dialog('close');
// dialog buttons
var buttons = [
{
text: rcmail.gettext('addresource', 'calendar'),
'class': 'mainaction save',
click: function() { rcmail.command('add-resource'); $dialog.dialog("close"); }
},
{
text: rcmail.gettext('close'),
'class': 'cancel',
click: function() { $dialog.dialog("close"); }
}
];
var resize = function() {
var container = $(rcmail.gui_objects.resourceinfocalendar);
container.fullCalendar('option', 'height', container.height() + 1);
};
// open jquery UI dialog
$dialog.dialog({
modal: true,
resizable: false, // prevents from Availability tab reflow bugs on resize
closeOnEscape: true,
title: rcmail.gettext('findresources', 'calendar'),
classes: {'ui-dialog': 'selection-dialog resources-dialog'},
open: function() {
rcmail.ksearch_blur();
$dialog.attr('aria-hidden', 'false');
// for Elastic
if ($('html.layout-small,html.layout-phone').length) {
$('#eventresourcesdialog .resource-selection').css('display', 'flex');
$('#eventresourcesdialog .resource-content').css('display', 'none');
}
setTimeout(resize, 50);
},
close: function() {
$dialog.dialog('destroy').attr('aria-hidden', 'true').hide();
},
resize: resize,
buttons: buttons,
width: 900,
height: 500
}).show();
$('.ui-dialog-buttonset button', $dialog.parent()).first().attr('id', 'rcmbtncalresadd');
me.dialog_resize($dialog.get(0), 540, Math.min(1000, $(window).width() - 50));
// set search query
$('#resourcesearchbox').val(search || '');
// initialize the treelist widget
if (!resources_treelist) {
resources_treelist = new rcube_treelist_widget(rcmail.gui_objects.resourceslist, {
id_prefix: 'rcres',
id_encode: rcmail.html_identifier_encode,
id_decode: rcmail.html_identifier_decode,
selectable: true,
save_state: true
});
resources_treelist.addEventListener('select', function(node) {
if (resources_data[node.id]) {
resource_showinfo(resources_data[node.id]);
rcmail.enable_command('add-resource', me.selected_event && $("#eventedit").is(':visible'));
// on elastic mobile display resource info box
if ($('html.layout-small,html.layout-phone').length) {
$('#eventresourcesdialog .resource-selection').css('display', 'none');
$('#eventresourcesdialog .resource-content').css('display', 'flex');
$(window).resize();
resize();
}
}
else {
rcmail.enable_command('add-resource', false);
$(rcmail.gui_objects.resourceinfo).hide();
$(rcmail.gui_objects.resourceownerinfo).hide();
$(rcmail.gui_objects.resourceinfocalendar).fullCalendar('removeEventSources');
}
});
// fetch (all) resource data from server
me.loading_lock = rcmail.set_busy(true, 'loading', me.loading_lock);
rcmail.http_request('resources-list', {}, me.loading_lock);
// register button
rcmail.register_button('add-resource', 'rcmbtncalresadd', 'button');
// initialize resource calendar display
var resource_cal = $(rcmail.gui_objects.resourceinfocalendar);
resource_cal.fullCalendar($.extend({}, fullcalendar_defaults, {
header: { left: '', center: '', right: '' },
height: resource_cal.height() + 4,
defaultView: 'agendaWeek',
eventSources: [],
slotMinutes: 60,
allDaySlot: false,
eventRender: function(event, element, view) {
var title = rcmail.get_label(event.status, 'calendar');
element.addClass('status-' + event.status);
element.find('.fc-title').text(title);
element.attr('aria-label', me.event_date_text(event, true) + ': ' + title);
}
}));
$('#resource-calendar-prev').click(function(){
resource_cal.fullCalendar('prev');
return false;
});
$('#resource-calendar-next').click(function(){
resource_cal.fullCalendar('next');
return false;
});
}
else if (search) {
resource_search();
}
else {
resource_render_list(resources_index);
}
if (me.selected_event && me.selected_event.start) {
$(rcmail.gui_objects.resourceinfocalendar).fullCalendar('gotoDate', me.selected_event.start);
}
};
// render the resource details UI box
var resource_showinfo = function(resource)
{
// inline function to render a resource attribute
function render_attrib(value) {
if (typeof value == 'boolean') {
return value ? rcmail.get_label('yes') : rcmail.get_label('no');
}
return value;
}
if (rcmail.gui_objects.resourceinfo) {
var tr, table = $(rcmail.gui_objects.resourceinfo).show().find('tbody').html(''),
attribs = $.extend({ name:resource.name }, resource.attributes||{})
attribs.description = resource.description;
for (var k in attribs) {
if (typeof attribs[k] == 'undefined')
continue;
table.append($('<tr>').addClass(k + ' form-group row')
.append('<td class="title col-sm-4"><label class="col-form-label">' + Q(ucfirst(rcmail.get_label(k, 'calendar'))) + '</label></td>')
.append('<td class="value col-sm-8 form-control-plaintext">' + text2html(render_attrib(attribs[k])) + '</td>')
);
}
$(rcmail.gui_objects.resourceownerinfo).hide();
$(rcmail.gui_objects.resourceinfocalendar).fullCalendar('removeEventSources');
if (resource.owner) {
// display cached data
if (resource_owners[resource.owner]) {
resource_owner_load(resource_owners[resource.owner]);
}
else {
// fetch owner data from server
me.loading_lock = rcmail.set_busy(true, 'loading', me.loading_lock);
rcmail.http_request('resources-owner', { _id: resource.owner }, me.loading_lock);
}
}
// load resource calendar
resources_events_source.url = "./?_task=calendar&_action=resources-calendar&_id="+urlencode(resource.ID);
$(rcmail.gui_objects.resourceinfocalendar).fullCalendar('addEventSource', resources_events_source);
}
};
// callback from server for resource listing
var resource_data_load = function(data)
{
var resources_tree = {};
// store data by ID
$.each(data, function(i, rec) {
resources_data[rec.ID] = rec;
// assign parent-relations
if (rec.members) {
$.each(rec.members, function(j, m){
resources_tree[m] = rec.ID;
});
}
});
// walk the parent-child tree to determine the depth of each node
$.each(data, function(i, rec) {
rec._depth = 0;
if (resources_tree[rec.ID])
rec.parent_id = resources_tree[rec.ID];
var parent_id = resources_tree[rec.ID];
while (parent_id) {
rec._depth++;
parent_id = resources_tree[parent_id];
}
});
// sort by depth, collection and name
data.sort(function(a,b) {
var j = a._type == 'collection' ? 1 : 0,
k = b._type == 'collection' ? 1 : 0,
d = a._depth - b._depth;
if (!d) d = (k - j);
if (!d) d = b.name < a.name ? 1 : -1;
return d;
});
$.each(data, function(i, rec) {
resources_index.push(rec.ID);
});
// apply search filter...
if ($('#resourcesearchbox').val() != '')
resource_search();
else // ...or render full list
resource_render_list(resources_index);
rcmail.set_busy(false, null, me.loading_lock);
};
// renders the given list of resource records into the treelist
var resource_render_list = function(index) {
var rec, link;
resources_treelist.reset();
$.each(index, function(i, dn) {
if (rec = resources_data[dn]) {
link = $('<a>').attr('href', '#')
.attr('rel', rec.ID)
.html(Q(rec.name));
resources_treelist.insert({ id:rec.ID, html:link, classes:[rec._type], collapsed:true }, rec.parent_id, false);
}
});
};
// callback from server for owner information display
var resource_owner_load = function(data)
{
if (data) {
// cache this!
resource_owners[data.ID] = data;
var table = $(rcmail.gui_objects.resourceownerinfo).find('tbody').html('');
for (var k in data) {
if (k == 'event' || k == 'ID')
continue;
table.append($('<tr>').addClass(k)
.append('<td class="title">' + Q(ucfirst(rcmail.get_label(k, 'calendar'))) + '</td>')
.append('<td class="value">' + text2html(data[k]) + '</td>')
);
}
table.parent().show();
}
}
// quick-filter the loaded resource data
var resource_search = function()
{
var dn, rec, dataset = [],
q = $('#resourcesearchbox').val().toLowerCase();
if (q.length && resources_data) {
// search by iterating over all resource records
for (dn in resources_data) {
rec = resources_data[dn];
if ((rec.name && String(rec.name).toLowerCase().indexOf(q) >= 0)
|| (rec.email && String(rec.email).toLowerCase().indexOf(q) >= 0)
|| (rec.description && String(rec.description).toLowerCase().indexOf(q) >= 0)
) {
dataset.push(rec.ID);
}
}
resource_render_list(dataset);
// select single match
if (dataset.length == 1) {
resources_treelist.select(dataset[0]);
}
}
else {
$('#resourcesearchbox').val('');
}
};
//
var reset_resource_search = function()
{
$('#resourcesearchbox').val('').focus();
resource_render_list(resources_index);
};
//
var add_resource2event = function()
{
var resource = resources_data[resources_treelist.get_selection()];
if (resource)
add_attendee($.extend({ role:'REQ-PARTICIPANT', status:'NEEDS-ACTION', cutype:'RESOURCE' }, resource));
}
var is_this_me = function(email)
{
if (settings.identity.emails.indexOf(';'+email) >= 0
|| (settings.identity.ownedResources && settings.identity.ownedResources.indexOf(';'+email) >= 0)
) {
return true;
}
return false;
};
// when the user accepts or declines an event invitation
var event_rsvp = function(response, delegate, replymode, event)
{
var btn;
if (typeof response == 'object') {
btn = $(response);
response = btn.attr('rel')
}
else {
btn = $('#event-rsvp input.button[rel='+response+']');
}
// show menu to select rsvp reply mode (current or all)
if (me.selected_event && me.selected_event.recurrence && !replymode) {
rcube_libcalendaring.itip_rsvp_recurring(btn, function(resp, mode) {
event_rsvp(resp, null, mode, event);
}, event);
return;
}
if (me.selected_event && me.selected_event.attendees && response) {
// bring up delegation dialog
if (response == 'delegated' && !delegate) {
rcube_libcalendaring.itip_delegate_dialog(function(data) {
data.rsvp = data.rsvp ? 1 : '';
event_rsvp('delegated', data, replymode, event);
});
return;
}
// update attendee status
attendees = [];
for (var data, i=0; i < me.selected_event.attendees.length; i++) {
data = me.selected_event.attendees[i];
//FIXME this can only work if there is a single resource per invitation
if (is_this_me(String(data.email).toLowerCase())) {
data.status = response.toUpperCase();
data.rsvp = 0; // unset RSVP flag
if (data.status == 'DELEGATED') {
data['delegated-to'] = delegate.to;
data.rsvp = delegate.rsvp
}
else {
if (data['delegated-to']) {
delete data['delegated-to'];
if (data.role == 'NON-PARTICIPANT' && data.status != 'DECLINED')
data.role = 'REQ-PARTICIPANT';
}
}
attendees.push(i)
}
else if (response != 'DELEGATED' && data['delegated-from'] &&
settings.identity.emails.indexOf(';'+String(data['delegated-from']).toLowerCase()) >= 0) {
delete data['delegated-from'];
}
// set free_busy status to transparent if declined (#4425)
if (data.status == 'DECLINED' || data.role == 'NON-PARTICIPANT') {
me.selected_event.free_busy = 'free';
}
else {
me.selected_event.free_busy = 'busy';
}
}
// submit status change to server
var submit_data = $.extend({}, { source:null, comment:$('#reply-comment-event-rsvp').val(), _savemode: replymode || 'all' }, (delegate || {})),
submit_items = 'id,uid,_instance,calendar,_mbox,_uid,_part,attendees,free_busy,allDay',
noreply = $('#noreply-event-rsvp:checked').length ? 1 : 0;
// Submit only that data we really need
$.each(submit_items.split(','), function() {
if (this in me.selected_event)
submit_data[this] = me.selected_event[this];
});
// import event from mail (temporary iTip event)
if (submit_data._mbox && submit_data._uid) {
me.saving_lock = rcmail.set_busy(true, 'calendar.savingdata');
rcmail.http_post('mailimportitip', {
_mbox: submit_data._mbox,
_uid: submit_data._uid,
_part: submit_data._part,
_status: response,
_to: (delegate ? delegate.to : null),
_rsvp: (delegate && delegate.rsvp) ? 1 : 0,
_noreply: noreply,
_comment: submit_data.comment,
_instance: submit_data._instance,
_savemode: submit_data._savemode
});
}
else if (settings.invitation_calendars) {
update_event('rsvp', submit_data, { status:response, noreply:noreply, attendees:attendees });
}
else {
me.saving_lock = rcmail.set_busy(true, 'calendar.savingdata');
rcmail.http_post('calendar/event', { action:'rsvp', e:submit_data, status:response, attendees:attendees, noreply:noreply });
}
event_show_dialog(me.selected_event);
}
};
// add the given date to the RDATE list
var add_rdate = function(date)
{
var li = $('<li>')
.attr('data-value', date2servertime(date))
.append($('<span>').text(format_date(date, settings.date_format)))
.appendTo('#edit-recurrence-rdates');
$('<a>').attr('href', '#del')
.addClass('iconbutton delete')
.html(rcmail.get_label('delete', 'calendar'))
.attr('title', rcmail.get_label('delete', 'calendar'))
.appendTo(li);
};
// re-sort the list items by their 'data-value' attribute
var sort_rdates = function()
{
var mylist = $('#edit-recurrence-rdates'),
listitems = mylist.children('li').get();
listitems.sort(function(a, b) {
var compA = $(a).attr('data-value');
var compB = $(b).attr('data-value');
return (compA < compB) ? -1 : (compA > compB) ? 1 : 0;
})
$.each(listitems, function(idx, item) { mylist.append(item); });
}
// remove the link reference matching the given uri
function remove_link(elem)
{
var $elem = $(elem), uri = $elem.attr('data-uri');
me.selected_event.links = $.grep(me.selected_event.links, function(link) { return link.uri != uri; });
// remove UI list item
$elem.hide().closest('li').addClass('deleted');
}
// post the given event data to server
var update_event = function(action, data, add)
{
me.saving_lock = rcmail.set_busy(true, 'calendar.savingdata');
rcmail.http_post('calendar/event', $.extend({ action:action, e:data }, (add || {})));
// render event temporarily into the calendar
if ((data.start && data.end) || data.id) {
var tmp, event = data.id ? $.extend(fc.fullCalendar('clientEvents', data.id)[0], data) : data;
if (data.start)
event.start = data.start;
if (data.end)
event.end = data.end;
if (data.allDay !== undefined)
event.allDay = !!data.allDay; // must be boolean for fullcalendar
// For fullCalendar all-day event's end date must be exclusive
if (event.allDay && data.end && (tmp = moment(data.end)) && tmp.format('Hms') !== '000') {
event.end = moment().year(tmp.year()).month(tmp.month()).date(tmp.date()).hour(0).minute(0).second(0).add(1, 'days');
}
event.editable = false;
event.temp = true;
event.className = ['fc-event-temp'];
fc.fullCalendar(data.id ? 'updateEvent' : 'renderEvent', event);
// mark all recurring instances as temp
if (event.recurrence || event.recurrence_id) {
var base_id = event.recurrence_id ? event.recurrence_id : String(event.id).replace(/-\d+(T\d{6})?$/, '');
$.each(fc.fullCalendar('clientEvents', function(e){ return e.id == base_id || e.recurrence_id == base_id; }), function(i,ev) {
ev.temp = true;
ev.editable = false;
event.className.push('fc-event-temp');
fc.fullCalendar('updateEvent', ev);
});
}
}
};
// mouse-click handler to check if the show dialog is still open and prevent default action
var dialog_check = function(e)
{
var showd = $("#eventshow");
if (showd.is(':visible') && !$(e.target).closest('.ui-dialog').length && !$(e.target).closest('.popupmenu').length) {
showd.dialog('close');
e.stopImmediatePropagation();
ignore_click = true;
return false;
}
else if (ignore_click) {
window.setTimeout(function(){ ignore_click = false; }, 20);
return false;
}
return true;
};
// display confirm dialog when modifying/deleting an event
var update_event_confirm = function(action, event, data)
{
// Allow other plugins to do actions here
// E.g. when you move/resize the event init wasn't called
// but we need it as some plugins may modify user identities
// we depend on here (kolab_delegation)
rcmail.triggerEvent('calendar-event-init', {o: event});
if (!data) data = event;
var decline = false, notify = false, html = '', cal = me.calendars[event.calendar],
_is_invitation = String(event.calendar).match(/^--invitation--(declined|pending)/) && RegExp.$1,
_has_attendees = me.has_attendees(event),
_is_attendee = _has_attendees && me.is_attendee(event),
_is_organizer = me.is_organizer(event);
// event has attendees, ask whether to notify them
if (_has_attendees) {
var checked = (settings.itip_notify & 1 ? ' checked="checked"' : '');
if (action == 'remove' && cal.group != 'shared' && !_is_organizer && _is_attendee && _is_invitation != 'declined') {
decline = true;
checked = event.status != 'CANCELLED' ? checked : '';
html += '<div class="message dialog-message ui alert boxwarning">' +
'<label><input class="confirm-attendees-decline pretty-checkbox" type="checkbox"' + checked + ' value="1" name="decline" /> ' +
rcmail.gettext('itipdeclineevent', 'calendar') +
'</label></div>';
}
else if (_is_organizer) {
notify = true;
if (settings.itip_notify & 2) {
html += '<div class="message dialog-message ui alert boxwarning">' +
'<label><input class="confirm-attendees-donotify pretty-checkbox" type="checkbox"' + checked + ' value="1" name="notify" /> ' +
rcmail.gettext((action == 'remove' ? 'sendcancellation' : 'sendnotifications'), 'calendar') +
'</label></div>';
}
else {
data._notify = settings.itip_notify;
}
}
else if (cal.group != 'shared' && !_is_invitation) {
html += '<div class="message dialog-message ui alert boxwarning">' + $('#edit-localchanges-warning').html() + '</div>';
data._notify = 0;
}
}
// recurring event: user needs to select the savemode
if (event.recurrence) {
var future_disabled = '', message_label = (action == 'remove' ? 'removerecurringeventwarning' : 'changerecurringeventwarning');
// disable the 'future' savemode if I'm an attendee
// reason: no calendaring system supports the thisandfuture range parameter in iTip REPLY
if (action == 'remove' && !_is_organizer && _is_attendee) {
future_disabled = ' disabled';
}
html += '<div class="message dialog-message ui alert boxwarning">' + rcmail.gettext(message_label, 'calendar') + '</div>' +
'<div class="savemode">' +
'<a href="#current" class="button btn btn-secondary">' + rcmail.gettext('currentevent', 'calendar') + '</a>' +
'<a href="#future" class="button btn btn-secondary' + future_disabled + '">' + rcmail.gettext('futurevents', 'calendar') + '</a>' +
'<a href="#all" class="button btn btn-secondary">' + rcmail.gettext('allevents', 'calendar') + '</a>' +
(action != 'remove' ? '<a href="#new" class="button btn btn-secondary">' + rcmail.gettext('saveasnew', 'calendar') + '</a>' : '') +
'</div>';
}
// show dialog
if (html) {
var $dialog = $('<div>').html(html);
$dialog.find('a.button').filter(':not(.disabled)').click(function(e) {
data._savemode = String(this.href).replace(/.+#/, '');
// open event edit dialog when saving as new
if (data._savemode == 'new') {
event._savemode = 'new';
event_edit_dialog('edit', event);
fc.fullCalendar('refetchEvents');
}
else {
if ($dialog.find('input.confirm-attendees-donotify').length)
data._notify = $dialog.find('input.confirm-attendees-donotify').get(0).checked ? 1 : 0;
if (decline) {
data._decline = $dialog.find('input.confirm-attendees-decline:checked').length;
data._notify = 0;
}
update_event(action, data);
}
$dialog.dialog("close");
return false;
});
var buttons = [];
if (!event.recurrence) {
buttons.push({
text: rcmail.gettext((action == 'remove' ? 'delete' : 'save'), 'calendar'),
'class': action == 'remove' ? 'delete mainaction' : 'save mainaction',
click: function() {
data._notify = notify && $dialog.find('input.confirm-attendees-donotify:checked').length ? 1 : 0;
data._decline = decline && $dialog.find('input.confirm-attendees-decline:checked').length ? 1 : 0;
update_event(action, data);
$(this).dialog("close");
}
});
}
buttons.push({
text: rcmail.gettext('cancel', 'calendar'),
'class': 'cancel',
click: function() {
$(this).dialog("close");
}
});
$dialog.dialog({
modal: true,
width: 460,
dialogClass: 'warning',
title: rcmail.gettext((action == 'remove' ? 'removeeventconfirm' : 'changeeventconfirm'), 'calendar'),
buttons: buttons,
open: function() {
setTimeout(function(){
$dialog.parent().find('button:not(.ui-dialog-titlebar-close)').first().focus();
}, 5);
},
close: function(){
$dialog.dialog("destroy").remove();
if (!rcmail.busy)
fc.fullCalendar('refetchEvents');
}
}).addClass('event-update-confirm').show();
return false;
}
// show regular confirm box when deleting
else if (action == 'remove' && !cal.undelete) {
if (!confirm(rcmail.gettext('deleteventconfirm', 'calendar')))
return false;
}
// do update
update_event(action, data);
return true;
};
/*** public methods ***/
/**
* Remove saving lock and free the UI for new input
*/
this.unlock_saving = function()
{
if (me.saving_lock)
rcmail.set_busy(false, null, me.saving_lock);
};
// opens the given calendar in a popup dialog
this.quickview = function(id, shift)
{
var src, in_quickview = false;
$.each(this.quickview_sources, function(i,cal) {
if (cal.id == id) {
in_quickview = true;
src = cal;
}
});
// remove source from quickview
if (in_quickview && shift) {
this.quickview_sources = $.grep(this.quickview_sources, function(src) { return src.id != id; });
}
else {
if (!shift) {
// remove all current quickview event sources
if (this.quickview_active) {
fc.fullCalendar('removeEventSources');
}
this.quickview_sources = [];
// uncheck all active quickview icons
calendars_list.container.find('div.focusview')
.add('#calendars .searchresults div.focusview')
.removeClass('focusview')
.find('a.quickview').attr('aria-checked', 'false');
}
if (!in_quickview) {
// clone and modify calendar properties
src = $.extend({}, this.calendars[id]);
src.url += '&_quickview=1';
this.quickview_sources.push(src);
}
}
// disable quickview
if (this.quickview_active && !this.quickview_sources.length) {
// register regular calendar event sources
$.each(this.calendars, function(k, cal) {
if (cal.active)
fc.fullCalendar('addEventSource', cal);
});
this.quickview_active = false;
$('body').removeClass('quickview-active');
// uncheck all active quickview icons
calendars_list.container.find('div.focusview')
.add('#calendars .searchresults div.focusview')
.removeClass('focusview')
.find('a.quickview').attr('aria-checked', 'false');
}
// activate quickview
else if (!this.quickview_active) {
// remove regular calendar event sources
fc.fullCalendar('removeEventSources');
// register quickview event sources
$.each(this.quickview_sources, function(i, src) {
fc.fullCalendar('addEventSource', src);
});
this.quickview_active = true;
$('body').addClass('quickview-active');
}
// update quickview sources
else if (in_quickview) {
fc.fullCalendar('removeEventSource', src);
}
else if (src) {
fc.fullCalendar('addEventSource', src);
}
// activate quickview icon
if (this.quickview_active) {
$(calendars_list.get_item(id)).find('.calendar').first()
.add('#calendars .searchresults .cal-' + id)
[in_quickview ? 'removeClass' : 'addClass']('focusview')
.find('a.quickview').attr('aria-checked', in_quickview ? 'false' : 'true');
}
};
// disable quickview mode
function reset_quickview()
{
// remove all current quickview event sources
if (me.quickview_active) {
fc.fullCalendar('removeEventSources');
me.quickview_sources = [];
}
// register regular calendar event sources
$.each(me.calendars, function(k, cal) {
if (cal.active)
fc.fullCalendar('addEventSource', cal);
});
// uncheck all active quickview icons
calendars_list.container.find('div.focusview')
.add('#calendars .searchresults div.focusview')
.removeClass('focusview')
.find('a.quickview').attr('aria-checked', 'false');
me.quickview_active = false;
$('body').removeClass('quickview-active');
};
//public method to show the print dialog.
this.print_calendars = function(view)
{
if (!view) view = fc.fullCalendar('getView').name;
var date = fc.fullCalendar('getDate').toDate();
rcmail.open_window(rcmail.url('print', {
view: view,
date: date2unixtime(date),
range: settings.agenda_range,
search: this.search_query
}), true, true);
};
// public method to bring up the new event dialog
this.add_event = function(templ) {
if (this.selected_calendar) {
var now = new Date();
var date = fc.fullCalendar('getDate').toDate();
date.setHours(now.getHours()+1);
date.setMinutes(0);
var end = new Date(date.getTime());
end.setHours(date.getHours()+1);
event_edit_dialog('new', $.extend({ start:date, end:end, allDay:false, calendar:this.selected_calendar }, templ || {}));
}
};
// delete the given event after showing a confirmation dialog
this.delete_event = function(event) {
// show confirm dialog for recurring events, use jquery UI dialog
return update_event_confirm('remove', event, { id:event.id, calendar:event.calendar, attendees:event.attendees });
};
// opens a jquery UI dialog with event properties (or empty for creating a new calendar)
this.calendar_edit_dialog = function(calendar)
{
if (!calendar)
calendar = { name:'', color:'cc0000', editable:true, showalarms:true };
var title = rcmail.gettext((calendar.id ? 'editcalendar' : 'createcalendar'), 'calendar'),
params = {action: calendar.id ? 'form-edit' : 'form-new', c: {id: calendar.id}, _framed: 1},
$dialog = $('<iframe>').attr('src', rcmail.url('calendar', params)).on('load', function() {
var contents = $(this).contents();
contents.find('#calendar-name')
.prop('disabled', !calendar.editable)
.val(calendar.editname || calendar.name)
.select();
contents.find('#calendar-color')
.val(calendar.color);
contents.find('#calendar-showalarms')
.prop('checked', calendar.showalarms);
}),
save_func = function() {
var data,
form = $dialog.contents().find('#calendarpropform'),
name = form.find('#calendar-name');
// form is not loaded
if (!form || !form.length)
return false;
// do some input validation
if (!name.val() || name.val().length < 2) {
rcmail.alert_dialog(rcmail.gettext('invalidcalendarproperties', 'calendar'), function() {
name.select();
});
return false;
}
// post data to server
data = form.serializeJSON();
if (data.color)
data.color = data.color.replace(/^#/, '');
if (calendar.id)
data.id = calendar.id;
me.saving_lock = rcmail.set_busy(true, 'calendar.savingdata');
rcmail.http_post('calendar', { action:(calendar.id ? 'edit' : 'new'), c:data });
$dialog.dialog("close");
};
rcmail.simple_dialog($dialog, title, save_func, {
width: 600,
height: 400
});
};
this.calendar_remove = function(calendar)
{
this.calendar_destroy_source(calendar.id);
rcmail.http_post('calendar', { action:'subscribe', c:{ id:calendar.id, active:0, permanent:0, recursive:1 } });
return true;
};
this.calendar_delete = function(calendar)
{
var label = calendar.children ? 'deletecalendarconfirmrecursive' : 'deletecalendarconfirm';
rcmail.confirm_dialog(rcmail.gettext(label, 'calendar'), 'delete', function() {
rcmail.http_post('calendar', { action:'delete', c:{ id:calendar.id } });
return true;
});
return false;
};
this.calendar_refresh_source = function(id)
{
// got race-conditions fc.currentFetchID when using refetchEventSources,
// so we remove and add the source instead
// fc.fullCalendar('refetchEventSources', me.calendars[id]);
// TODO: Check it again with fullcalendar >= 3.9
fc.fullCalendar('removeEventSource', me.calendars[id]);
fc.fullCalendar('addEventSource', me.calendars[id]);
};
this.calendar_destroy_source = function(id)
{
var delete_ids = [];
if (this.calendars[id]) {
// find sub-calendars
if (this.calendars[id].children) {
for (var child_id in this.calendars) {
if (String(child_id).indexOf(id) == 0)
delete_ids.push(child_id);
}
}
else {
delete_ids.push(id);
}
}
// delete all calendars in the list
for (var i=0; i < delete_ids.length; i++) {
id = delete_ids[i];
calendars_list.remove(id);
fc.fullCalendar('removeEventSource', this.calendars[id]);
$('#edit-calendar option[value="'+id+'"]').remove();
delete this.calendars[id];
}
if (this.selected_calendar == id) {
this.init_calendars(true);
}
};
// open a dialog to upload an .ics file with events to be imported
this.import_events = function(calendar)
{
// close show dialog first
var $dialog = $("#eventsimport"),
form = rcmail.gui_objects.importform;
if ($dialog.is(':ui-dialog'))
$dialog.dialog('close');
if (calendar)
$('#event-import-calendar').val(calendar.id);
var buttons = [
{
text: rcmail.gettext('import', 'calendar'),
'class' : 'mainaction import',
click: function() {
if (form && form.elements._data.value) {
rcmail.async_upload_form(form, 'import_events', function(e) {
rcmail.set_busy(false, null, me.saving_lock);
$('.ui-dialog-buttonpane button', $dialog.parent()).prop('disabled', false);
// display error message if no sophisticated response from server arrived (e.g. iframe load error)
if (me.import_succeeded === null)
rcmail.display_message(rcmail.get_label('importerror', 'calendar'), 'error');
});
// display upload indicator (with extended timeout)
var timeout = rcmail.env.request_timeout;
rcmail.env.request_timeout = 600;
me.import_succeeded = null;
me.saving_lock = rcmail.set_busy(true, 'uploading');
$('.ui-dialog-buttonpane button', $dialog.parent()).prop('disabled', true);
// restore settings
rcmail.env.request_timeout = timeout;
}
}
},
{
text: rcmail.gettext('cancel', 'calendar'),
'class': 'cancel',
click: function() { $dialog.dialog("close"); }
}
];
// open jquery UI dialog
$dialog.dialog({
modal: true,
resizable: false,
closeOnEscape: false,
title: rcmail.gettext('importevents', 'calendar'),
close: function() {
$('.ui-dialog-buttonpane button', $dialog.parent()).prop('disabled', false);
$dialog.dialog("destroy").hide();
},
buttons: buttons,
width: 520
}).show();
};
// callback from server if import succeeded
this.import_success = function(p)
{
this.import_succeeded = true;
$("#eventsimport:ui-dialog").dialog('close');
rcmail.set_busy(false, null, me.saving_lock);
rcmail.gui_objects.importform.reset();
if (p.refetch)
this.refresh(p);
};
// callback from server to report errors on import
this.import_error = function(p)
{
this.import_succeeded = false;
rcmail.set_busy(false, null, me.saving_lock);
rcmail.display_message(p.message || rcmail.get_label('importerror', 'calendar'), 'error');
}
// open a dialog to select calendars for export
this.export_events = function(calendar)
{
// close show dialog first
var $dialog = $("#eventsexport"),
form = rcmail.gui_objects.exportform;
if ($dialog.is(':ui-dialog'))
$dialog.dialog('close');
if (calendar)
$('#event-export-calendar').val(calendar.id);
$('#event-export-range').change(function(e){
var custom = $(this).val() == 'custom', input = $('#event-export-startdate');
$(this)[custom ? 'removeClass' : 'addClass']('rounded-right'); // Elastic/Bootstrap
input[custom ? 'show' : 'hide']();
if (custom)
input.select();
})
var buttons = [
{
text: rcmail.gettext('export', 'calendar'),
'class': 'mainaction export',
click: function() {
if (form) {
var start = 0, range = $('#event-export-range', this).val(),
source = $('#event-export-calendar').val(),
attachmt = $('#event-export-attachments').get(0).checked;
if (range == 'custom')
start = date2unixtime(me.parse_datetime('00:00', $('#event-export-startdate').val()));
else if (range > 0)
start = 'today -' + range + ' months';
rcmail.goto_url('export_events', { source:source, start:start, attachments:attachmt?1:0 }, false);
}
$dialog.dialog("close");
}
},
{
text: rcmail.gettext('cancel', 'calendar'),
'class': 'cancel',
click: function() { $dialog.dialog("close"); }
}
];
// open jquery UI dialog
$dialog.dialog({
modal: true,
resizable: false,
closeOnEscape: false,
title: rcmail.gettext('exporttitle', 'calendar'),
close: function() {
$('.ui-dialog-buttonpane button', $dialog.parent()).prop('disabled', false);
$dialog.dialog("destroy").hide();
},
buttons: buttons,
width: 520
}).show();
};
// download the selected event as iCal
this.event_download = function(event)
{
if (event && event.id) {
rcmail.goto_url('export_events', { source:event.calendar, id:event.id, attachments:1 }, false);
}
};
// open the message compose step with a calendar_event parameter referencing the selected event.
// the server-side plugin hook will pick that up and attach the event to the message.
this.event_sendbymail = function(event, e)
{
if (event && event.id) {
rcmail.command('compose', { _calendar_event:event._id }, e ? e.target : null, e);
}
};
// display the edit dialog, request 'new' action and pass the selected event
this.event_copy = function(event) {
if (event && event.id) {
var copy = $.extend(true, {}, event);
delete copy.id;
delete copy._id;
delete copy.created;
delete copy.changed;
delete copy.recurrence_id;
delete copy.attachments; // @TODO
$.each(copy.attendees, function (k, v) {
if (v.role != 'ORGANIZER') {
v.status = 'NEEDS-ACTION';
}
});
setTimeout(function() { event_edit_dialog('new', copy); }, 50);
}
};
// show URL of the given calendar in a dialog box
this.showurl = function(calendar)
{
if (calendar.feedurl) {
var dialog = $('#calendarurlbox').clone(true).removeClass('uidialog');
if (calendar.caldavurl) {
$('#caldavurl', dialog).val(calendar.caldavurl);
$('#calendarcaldavurl', dialog).show();
}
else {
$('#calendarcaldavurl', dialog).hide();
}
rcmail.simple_dialog(dialog, rcmail.gettext('showurl', 'calendar'), null, {
open: function() { $('#calfeedurl', dialog).val(calendar.feedurl).select(); },
cancel_button: 'close'
});
}
};
// show free-busy URL in a dialog box
this.showfburl = function()
{
var dialog = $('#fburlbox').clone(true);
rcmail.simple_dialog(dialog, rcmail.gettext('showfburl', 'calendar'), null, {
open: function() { $('#fburl', dialog).val(settings.freebusy_url).select(); },
cancel_button: 'close'
});
};
// refresh the calendar view after saving event data
this.refresh = function(p)
{
var source = me.calendars[p.source];
// helper function to update the given fullcalendar view
function update_view(view, event, source) {
var existing = view.fullCalendar('clientEvents', event._id);
if (existing.length) {
delete existing[0].temp;
delete existing[0].editable;
$.extend(existing[0], event);
view.fullCalendar('updateEvent', existing[0]);
// remove old recurrence instances
if (event.recurrence && !event.recurrence_id)
view.fullCalendar('removeEvents', function(e){ return e._id.indexOf(event._id+'-') == 0; });
}
else {
event.source = view.fullCalendar('getEventSourceById', source.id); // link with source
view.fullCalendar('renderEvent', event);
}
}
// remove temp events
fc.fullCalendar('removeEvents', function(e){ return e.temp; });
if (source && (p.refetch || (p.update && !source.active))) {
// activate event source if new event was added to an invisible calendar
if (this.quickview_active) {
// map source to the quickview_sources equivalent
$.each(this.quickview_sources, function(src) {
if (src.id == source.id) {
source = src;
return false;
}
});
}
else if (!source.active) {
source.active = true;
$('#rcmlical' + source.id + ' input').prop('checked', true);
}
fc.fullCalendar('refetchEventSources', source.id);
fetch_counts();
}
// add/update single event object
else if (source && p.update) {
var event = p.update;
// update main view
update_view(fc, event, source);
// update the currently displayed event dialog
if ($('#eventshow').is(':visible') && me.selected_event && me.selected_event.id == event.id)
event_show_dialog(event);
}
// refetch all calendars
else if (p.refetch) {
fc.fullCalendar('refetchEvents');
fetch_counts();
}
};
// modify query parameters for refresh requests
this.before_refresh = function(query)
{
var view = fc.fullCalendar('getView');
query.start = date2unixtime(view.start.toDate());
query.end = date2unixtime(view.end.toDate());
if (this.search_query)
query.q = this.search_query;
return query;
};
// callback from server providing event counts
this.update_counts = function(p)
{
$.each(p.counts, function(cal, count) {
var li = calendars_list.get_item(cal),
bubble = $(li).children('.calendar').find('span.count');
if (!bubble.length && count > 0) {
bubble = $('<span>')
.addClass('count')
.appendTo($(li).children('.calendar').first())
}
if (count > 0) {
bubble.text(count).show();
}
else {
bubble.text('').hide();
}
});
};
// callback after an iTip message event was imported
this.itip_message_processed = function(data)
{
// remove temporary iTip source
fc.fullCalendar('removeEventSource', this.calendars['--invitation--itip']);
$('#eventshow:ui-dialog').dialog('close');
this.selected_event = null;
// refresh destination calendar source
this.refresh({ source:data.calendar, refetch:true });
this.unlock_saving();
// process 'after_action' in mail task
if (window.opener && window.opener.rcube_libcalendaring)
window.opener.rcube_libcalendaring.itip_message_processed(data);
};
// reload the calendar view by keeping the current date/view selection
this.reload_view = function()
{
var query = { view: fc.fullCalendar('getView').name },
date = fc.fullCalendar('getDate');
if (date)
query.date = date2unixtime(date.toDate());
rcmail.redirect(rcmail.url('', query));
}
// update browser location to remember current view
this.update_state = function()
{
var query = { view: current_view },
date = fc.fullCalendar('getDate');
if (date)
query.date = date2unixtime(date.toDate());
if (window.history.replaceState)
window.history.replaceState({}, document.title, rcmail.url('', query).replace('&_action=', ''));
};
this.resource_search = resource_search;
this.reset_resource_search = reset_resource_search;
this.add_resource2event = add_resource2event;
this.resource_data_load = resource_data_load;
this.resource_owner_load = resource_owner_load;
/*** event searching ***/
// execute search
this.quicksearch = function()
{
if (rcmail.gui_objects.qsearchbox) {
var q = rcmail.gui_objects.qsearchbox.value;
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;
}
// remember current view
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');
// refetch events with new url (if not already triggered by changeView)
if (!this.is_loading)
fc.fullCalendar('refetchEvents');
}
else // empty search input equals reset
this.reset_quicksearch();
}
};
// reset search and get back to normal event listing
this.reset_quicksearch = function()
{
$(rcmail.gui_objects.qsearchbox).val('');
if (this._search_message)
rcmail.hide_message(this._search_message);
if (this.search_request) {
$.each(fc.fullCalendar('getEventSources'), function() {
this.url = this.url.replace(/&q=.+/, '');
me.calendars[this.id].url = this.url;
});
this.search_request = this.search_query = null;
fc.fullCalendar('refetchEvents');
}
};
// callback if all sources have been fetched from server
this.events_loaded = function()
{
if (this.search_request && !fc.fullCalendar('clientEvents').length) {
this._search_message = rcmail.display_message(rcmail.gettext('searchnoresults', 'calendar'), 'notice');
}
};
// adjust calendar view size
this.view_resize = function()
{
var footer = fc.fullCalendar('getView').name == 'list' ? $('#agendaoptions').outerHeight() : 0;
fc.fullCalendar('option', 'height', $('#calendar').height() - footer);
};
// mark the given calendar folder as selected
this.select_calendar = function(id, nolistupdate)
{
if (!nolistupdate)
calendars_list.select(id);
// trigger event hook
rcmail.triggerEvent('selectfolder', { folder:id, prefix:'rcmlical' });
this.selected_calendar = id;
rcmail.update_state({source: id});
rcmail.enable_command('addevent', this.calendars[id] && this.calendars[id].editable);
};
// register the given calendar to the current view
var add_calendar_source = function(cal)
{
var brightness, select, id = cal.id;
me.calendars[id] = $.extend({
url: rcmail.url('calendar/load_events', { source: id }),
id: id
}, cal);
if (fc && (cal.active || cal.subscribed)) {
if (cal.active)
fc.fullCalendar('addEventSource', me.calendars[id]);
var submit = { id: id, active: cal.active ? 1 : 0 };
if (cal.subscribed !== undefined)
submit.permanent = cal.subscribed ? 1 : 0;
rcmail.http_post('calendar', { action:'subscribe', c:submit });
}
// insert to #calendar-select options if writeable
select = $('#edit-calendar');
if (fc && me.has_permission(cal, 'i') && select.length && !select.find('option[value="'+id+'"]').length) {
$('<option>').attr('value', id).html(cal.name).appendTo(select);
}
}
// fetch counts for some calendars from the server
var fetch_counts = function()
{
if (count_sources.length) {
setTimeout(function() {
rcmail.http_request('calendar/count', { source:count_sources });
}, 500);
}
};
this.init_calendars = function(refresh)
{
var id, cal, active;
for (id in rcmail.env.calendars) {
cal = rcmail.env.calendars[id];
active = cal.active || false;
if (!refresh) {
add_calendar_source(cal);
// check active calendars
$('#rcmlical'+id+' > .calendar input').prop('checked', active);
if (active) {
event_sources.push(this.calendars[id]);
}
if (cal.counts) {
count_sources.push(id);
}
}
if (cal.editable && (!this.selected_calendar || refresh)) {
this.selected_calendar = id;
if (refresh) {
this.select_calendar(id);
refresh = false;
}
}
}
};
/*** Nextcloud Talk integration ***/
this.talk_room_create = function()
{
var lock = rcmail.set_busy(true, 'calendar.talkroomcreating');
rcmail.http_post('talk-room-create', { _name: $('#edit-title').val() }, lock);
};
this.talk_room_created = function(data)
{
if (data.url) {
$('#edit-location').val(data.url);
}
};
/*** startup code ***/
// initialize treelist widget that controls the calendars list
var widget_class = window.kolab_folderlist || rcube_treelist_widget;
calendars_list = new widget_class(rcmail.gui_objects.calendarslist, {
id_prefix: 'rcmlical',
selectable: true,
save_state: true,
keyboard: false,
searchbox: '#calendarlistsearch',
search_action: 'calendar/calendar',
search_sources: [ 'folders', 'users' ],
search_title: rcmail.gettext('calsearchresults','calendar')
});
calendars_list.addEventListener('select', function(node) {
if (node && node.id && me.calendars[node.id]) {
me.select_calendar(node.id, true);
rcmail.enable_command('calendar-edit', 'calendar-showurl', 'calendar-showfburl', true);
rcmail.enable_command('calendar-delete', me.calendars[node.id].editable);
rcmail.enable_command('calendar-remove', me.calendars[node.id] && me.calendars[node.id].removable);
}
});
calendars_list.addEventListener('insert-item', function(p) {
var cal = p.data;
if (cal && cal.id) {
add_calendar_source(cal);
// add css classes related to this calendar to document
if (cal.css) {
$('<style type="text/css"></style>')
.html(cal.css)
.appendTo('head');
}
}
});
calendars_list.addEventListener('subscribe', function(p) {
var cal;
if ((cal = me.calendars[p.id])) {
cal.subscribed = p.subscribed || false;
rcmail.http_post('calendar', { action:'subscribe', c:{ id:p.id, active:cal.active?1:0, permanent:cal.subscribed?1:0 } });
}
});
calendars_list.addEventListener('remove', function(p) {
if (me.calendars[p.id] && me.calendars[p.id].removable) {
me.calendar_remove(me.calendars[p.id]);
}
});
calendars_list.addEventListener('search-complete', function(data) {
if (data.length)
rcmail.display_message(rcmail.gettext('nrcalendarsfound','calendar').replace('$nr', data.length), 'voice');
else
rcmail.display_message(rcmail.gettext('nocalendarsfound','calendar'), 'notice');
});
calendars_list.addEventListener('click-item', function(event) {
// handle clicks on quickview icon: temprarily add this source and open in quickview
if ($(event.target).hasClass('quickview') && event.data) {
if (!me.calendars[event.data.id]) {
event.data.readonly = true;
event.data.active = false;
event.data.subscribed = false;
add_calendar_source(event.data);
}
me.quickview(event.data.id, event.shiftKey || event.metaKey || event.ctrlKey);
return false;
}
});
// init (delegate) event handler on calendar list checkboxes
$(rcmail.gui_objects.calendarslist).on('click', 'input[type=checkbox]', function(e) {
e.stopPropagation();
if (me.quickview_active) {
this.checked = !this.checked;
return false;
}
var id = this.value;
// add or remove event source on click
if (me.calendars[id]) {
// adjust checked state of original list item
if (calendars_list.is_search()) {
calendars_list.container.find('input[value="'+id+'"]').prop('checked', this.checked);
}
me.calendars[id].active = this.checked;
// add/remove event source
fc.fullCalendar(this.checked ? 'addEventSource' : 'removeEventSource', me.calendars[id]);
rcmail.http_post('calendar', {action: 'subscribe', c: {id: id, active: this.checked ? 1 : 0}});
}
})
.on('keypress', 'input[type=checkbox]', function(e) {
// select calendar on <Enter>
if (e.keyCode == 13) {
calendars_list.select(this.value);
return rcube_event.cancel(e);
}
})
// init (delegate) event handler on quickview links
.on('click', 'a.quickview', function(e) {
var id = $(this).closest('li').attr('id').replace(/^rcmlical/, '');
if (calendars_list.is_search())
id = id.replace(/--xsR$/, '');
if (me.calendars[id])
me.quickview(id, e.shiftKey || e.metaKey || e.ctrlKey);
if (!rcube_event.is_keyboard(e) && this.blur)
this.blur();
e.stopPropagation();
return false;
});
// register dbl-click handler to open calendar edit dialog
- $(rcmail.gui_objects.calendarslist).on('dblclick', ':not(.virtual) > .calname', function(e){
+ $(rcmail.gui_objects.calendarslist).on('dblclick', ':not(.virtual) > .calname', function(e) {
var id = $(this).closest('li').attr('id').replace(/^rcmlical/, '');
- me.calendar_edit_dialog(me.calendars[id]);
+ if (me.calendars[id] && me.calendars[id].driver != 'caldav')
+ me.calendar_edit_dialog(me.calendars[id]);
});
// Make Elastic checkboxes pretty
if (window.UI && UI.pretty_checkbox) {
$(rcmail.gui_objects.calendarslist).find('input[type=checkbox]').each(function() {
UI.pretty_checkbox(this);
});
calendars_list.addEventListener('add-item', function(prop) {
UI.pretty_checkbox($(prop.li).find('input'));
});
}
// create list of event sources AKA calendars
this.init_calendars();
// select default calendar
if (rcmail.env.source && this.calendars[rcmail.env.source])
this.selected_calendar = rcmail.env.source;
else if (settings.default_calendar && this.calendars[settings.default_calendar] && this.calendars[settings.default_calendar].editable)
this.selected_calendar = settings.default_calendar;
if (this.selected_calendar)
this.select_calendar(this.selected_calendar);
var viewdate = new Date();
if (rcmail.env.date)
viewdate.setTime(fromunixtime(rcmail.env.date));
// add source with iTip event data for rendering
if (rcmail.env.itip_events && rcmail.env.itip_events.length) {
me.calendars['--invitation--itip'] = {
events: rcmail.env.itip_events,
color: 'ffffff',
editable: false,
rights: 'lrs',
attendees: true
};
event_sources.push(me.calendars['--invitation--itip']);
}
// initalize the fullCalendar plugin
var fc = $('#calendar').fullCalendar($.extend({}, fullcalendar_defaults, {
header: {
right: 'prev,next today',
center: 'title',
left: 'agendaDay,agendaWeek,month,list'
},
defaultDate: viewdate,
height: $('#calendar').height(),
eventSources: event_sources,
selectable: true,
selectHelper: false,
loading: function(isLoading) {
me.is_loading = isLoading;
this._rc_loading = rcmail.set_busy(isLoading, me.search_request ? 'searching' : 'loading', this._rc_loading);
// trigger callback (using timeout, otherwise clientEvents is always empty)
if (!isLoading)
setTimeout(function() { me.events_loaded(); }, 20);
},
// callback for date range selection
select: function(start, end, e, view) {
var range_select = (start.hasTime() || start != end)
if (dialog_check(e) && range_select)
event_edit_dialog('new', { start:start, end:end, allDay:!start.hasTime(), calendar:me.selected_calendar });
if (range_select || ignore_click)
view.calendar.unselect();
},
// callback for clicks in all-day box
dayClick: function(date, e, view) {
var now = new Date().getTime();
if (now - day_clicked_ts < 400 && day_clicked == date.toDate().getTime()) { // emulate double-click on day
var enddate = new Date();
if (date.hasTime())
enddate.setTime(date.toDate().getTime() + DAY_MS - 60000);
return event_edit_dialog('new', { start:date, end:enddate, allDay:!date.hasTime(), calendar:me.selected_calendar });
}
if (!ignore_click) {
view.calendar.gotoDate(date);
if (day_clicked && new Date(day_clicked).getMonth() != date.toDate().getMonth())
view.calendar.select(date, date);
}
day_clicked = date.toDate().getTime();
day_clicked_ts = now;
},
// callback when an event was dragged and finally dropped
eventDrop: function(event, delta, revertFunc) {
if (!event.end || event.end.diff(event.start) < 0) {
if (event.allDay)
event.end = moment(event.start).hour(13).minute(0).second(0);
else
event.end = moment(event.start).add(2, 'hours');
}
else if (event.allDay) {
event.end.subtract(1, 'days').hour(13);
}
if (event.allDay)
event.start.hour(12);
// send move request to server
var data = {
id: event.id,
calendar: event.calendar,
start: date2servertime(event.start),
end: date2servertime(event.end),
allDay: event.allDay?1:0
};
update_event_confirm('move', event, data);
},
// callback for event resizing
eventResize: function(event, delta) {
// sanitize event dates
if (event.allDay) {
event.start.hours(12);
event.end.hour(13).subtract(1, 'days');
}
// send resize request to server
var data = {
id: event.id,
calendar: event.calendar,
start: date2servertime(event.start),
end: date2servertime(event.end),
allDay: event.allDay?1:0
};
update_event_confirm('resize', event, data);
},
viewRender: function(view, element) {
$('#agendaoptions')[view.name == 'list' ? 'show' : 'hide']();
if (minical) {
window.setTimeout(function(){ minical.datepicker('setDate', fc.fullCalendar('getDate').toDate()); }, exec_deferred);
if (view.name != current_view)
me.view_resize();
current_view = view.name;
me.update_state();
}
var viewStart = moment(view.start);
$('#calendar .fc-prev-button').off('click').on('click', function() {
if (view.name == 'list')
fc.fullCalendar('gotoDate', viewStart.subtract(settings.agenda_range, 'days'));
else
fc.fullCalendar('prev');
});
$('#calendar .fc-next-button').off('click').on('click', function() {
if (view.name == 'list')
fc.fullCalendar('gotoDate', viewStart.add(settings.agenda_range, 'days'));
else
fc.fullCalendar('next');
});
},
eventAfterAllRender: function(view) {
if (view.name == 'list') {
// Fix colspan of headers after we added Location column
fc.find('tr.fc-list-heading > td').attr('colspan', 4);
}
}
}));
// if start date is changed, shift end date according to initial duration
var shift_enddate = function(dateText) {
var newstart = me.parse_datetime('0', dateText);
var newend = new Date(newstart.getTime() + $('#edit-startdate').data('duration') * 1000);
$('#edit-enddate').val(format_date(newend, me.settings.date_format));
event_times_changed();
};
// Set as calculateWeek to determine the week of the year based on the ISO 8601 definition.
// Uses the default $.datepicker.iso8601Week() function but takes firstDay setting into account.
// This is a temporary fix until http://bugs.jqueryui.com/ticket/8420 is resolved.
var iso8601Week = me.datepicker_settings.calculateWeek = function(date) {
var mondayOffset = Math.abs(1 - me.datepicker_settings.firstDay);
return $.datepicker.iso8601Week(new Date(date.getTime() + mondayOffset * 86400000));
};
var minical;
var init_calendar_ui = function()
{
var pretty_select = function(elem) {
// for Elastic
if (window.UI && UI.pretty_select) {
$(elem).addClass('form-control custom-select').each(function() { UI.pretty_select(this); });
}
};
// initialize small calendar widget using jQuery UI datepicker
minical = $('#datepicker').datepicker($.extend(me.datepicker_settings, {
inline: true,
changeMonth: true,
changeYear: true,
onSelect: function(dateText, inst) {
ignore_click = true;
var d = minical.datepicker('getDate');
fc.fullCalendar('gotoDate', d)
fc.fullCalendar('select', d, d);
setTimeout(function() { pretty_select($('select', minical)); }, 25);
},
onChangeMonthYear: function(year, month, inst) {
minical.data('year', year).data('month', month);
setTimeout(function() { pretty_select($('select', minical)); }, 25);
},
beforeShowDay: function(date) {
// TODO: this pretty_select() calls should be implemented in a different way
setTimeout(function() { pretty_select($('select', minical)); }, 25);
var view = fc.fullCalendar('getView'),
dt = moment(date).format('YYYYMMDD'),
active = view.start && view.start.format('YYYYMMDD') <= dt && view.end.format('YYYYMMDD') > dt;
return [ true, (active ? 'ui-datepicker-activerange ui-datepicker-active-' + view.name : ''), ''];
}
})) // set event handler for clicks on calendar week cell of the datepicker widget
.on('click', 'td.ui-datepicker-week-col', function(e) {
var cell = $(e.target);
if (e.target.tagName == 'TD') {
var base_date = minical.datepicker('getDate');
if (minical.data('month'))
base_date.setMonth(minical.data('month')-1);
if (minical.data('year'))
base_date.setYear(minical.data('year'));
base_date.setHours(12);
base_date.setDate(base_date.getDate() - ((base_date.getDay() + 6) % 7) + me.datepicker_settings.firstDay);
var base_kw = iso8601Week(base_date),
target_kw = parseInt(cell.html()),
wdiff = target_kw - base_kw;
if (wdiff > 10) // year jump
base_date.setYear(base_date.getFullYear() - 1);
else if (wdiff < -10)
base_date.setYear(base_date.getFullYear() + 1);
// select monday of the chosen calendar week
var day_off = base_date.getDay() - me.datepicker_settings.firstDay,
date = new Date(base_date.getTime() - day_off * DAY_MS + wdiff * 7 * DAY_MS);
fc.fullCalendar('changeView', 'agendaWeek', date);
minical.datepicker('setDate', date);
setTimeout(function() { pretty_select($('select', minical)); }, 25);
}
});
minical.find('.ui-datepicker-inline').attr('aria-labelledby', 'aria-label-minical');
if (rcmail.env.date) {
var viewdate = new Date();
viewdate.setTime(fromunixtime(rcmail.env.date));
minical.datepicker('setDate', viewdate);
}
// init event dialog
var tab_change = function(event, ui) {
// newPanel.selector for jQuery-UI 1.10, newPanel.attr('id') for jQuery-UI 1.12, href for Bootstrap tabs
var tab = (ui ? String(ui.newPanel.selector || ui.newPanel.attr('id')) : $(event.target).attr('href'))
.replace(/^#?event-panel-/, '').replace(/s$/, '');
var has_real_attendee = function(attendees) {
for (var i=0; i < (attendees ? attendees.length : 0); i++) {
if (attendees[i].cutype != 'RESOURCE')
return true;
}
};
if (tab == 'attendee' || tab == 'resource') {
if (!rcube_event.is_keyboard(event))
$('#edit-'+tab+'-name').select();
// update free-busy status if needed
if (freebusy_ui.needsupdate && me.selected_event)
update_freebusy_status(me.selected_event);
// add current user as organizer if non added yet
if (tab == 'attendee' && !has_real_attendee(event_attendees)) {
add_attendee($.extend({ role:'ORGANIZER' }, settings.identity));
$('#edit-attendees-form .attendees-invitebox').show();
}
}
// reset autocompletion on tab change (#3389)
rcmail.ksearch_blur();
// display recurrence warning in recurrence tab only
if (tab == 'recurrence')
$('#edit-recurrence-frequency').change();
else
$('#edit-recurrence-syncstart').hide();
};
$('#eventedit:not([data-notabs])').tabs({activate: tab_change}); // Larry
$('#eventedit a.nav-link').on('show.bs.tab', tab_change); // Elastic
$('#edit-enddate').datepicker(me.datepicker_settings);
$('#edit-startdate').datepicker(me.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')](); event_times_changed(); });
// configure drop-down menu on time input fields based on jquery UI autocomplete
$('#edit-starttime, #edit-endtime').each(function() {
me.init_time_autocomplete(this, {
container: '#eventedit',
change: event_times_changed
});
});
// adjust end time when changing start
$('#edit-starttime').change(function(e) {
var dstart = $('#edit-startdate'),
newstart = me.parse_datetime(this.value, dstart.val()),
newend = new Date(newstart.getTime() + dstart.data('duration') * 1000);
$('#edit-endtime').val(format_date(newend, me.settings.time_format));
$('#edit-enddate').val(format_date(newend, me.settings.date_format));
event_times_changed();
});
// register events on alarms and recurrence fields
me.init_alarms_edit('#edit-alarms');
me.init_recurrence_edit('#eventedit');
// reload free-busy status when changing the organizer identity
$('#eventedit').on('change', '#edit-identities-list', function(e) {
var email = settings.identities[$(this).val()],
icon = $(this).closest('tr').find('.availability > *');
if (email && icon.length) {
icon.attr('data-email', email);
check_freebusy_status(icon, email, me.selected_event);
}
});
$('#event-export-startdate').datepicker(me.datepicker_settings);
// init attendees autocompletion
var ac_props;
// parallel autocompletion
if (rcmail.env.autocomplete_threads > 0) {
ac_props = {
threads: rcmail.env.autocomplete_threads,
sources: rcmail.env.autocomplete_sources
};
}
rcmail.init_address_input_events($('#edit-attendee-name'), ac_props);
rcmail.addEventListener('autocomplete_insert', function(e) {
var cutype, success = false;
if (e.field.name == 'participant') {
cutype = e.data && e.data.type == 'group' && e.result_type == 'person' ? 'GROUP' : 'INDIVIDUAL';
success = add_attendees(e.insert, { role:'REQ-PARTICIPANT', status:'NEEDS-ACTION', cutype:cutype });
}
else if (e.field.name == 'resource' && e.data && e.data.email) {
success = add_attendee($.extend(e.data, { role:'REQ-PARTICIPANT', status:'NEEDS-ACTION', cutype:'RESOURCE' }));
}
if (e.field && success) {
e.field.value = '';
}
});
$('#edit-attendee-add').click(function(){
var input = $('#edit-attendee-name');
rcmail.ksearch_blur();
if (add_attendees(input.val(), { role:'REQ-PARTICIPANT', status:'NEEDS-ACTION', cutype:'INDIVIDUAL' })) {
input.val('');
}
});
rcmail.init_address_input_events($('#edit-resource-name'), { action:'calendar/resources-autocomplete' });
$('#edit-resource-add').click(function(){
var input = $('#edit-resource-name');
rcmail.ksearch_blur();
if (add_attendees(input.val(), { role:'REQ-PARTICIPANT', status:'NEEDS-ACTION', cutype:'RESOURCE' })) {
input.val('');
}
});
$('#edit-resource-find').click(function(){
event_resources_dialog();
return false;
});
$('#resource-content a.nav-link').on('click', function() {
e.preventDefault();
$(this).tab('show');
});
// handle change of "send invitations" checkbox
$('#edit-attendees-invite').change(function() {
$('#edit-attendees-donotify,input.edit-attendee-reply').prop('checked', this.checked);
// hide/show comment field
$('#eventedit .attendees-commentbox')[this.checked ? 'show' : 'hide']();
});
// delegate change event to "send invitations" checkbox
$('#edit-attendees-donotify').change(function() {
$('#edit-attendees-invite').click();
return false;
});
$('#edit-attendee-schedule').click(function(){
event_freebusy_dialog();
});
$('#schedule-freebusy-prev').html('◄').click(function() { render_freebusy_grid(-1); });
$('#schedule-freebusy-next').html('►').click(function() { render_freebusy_grid(1); });
$('#schedule-find-prev').click(function() { freebusy_find_slot(-1); });
$('#schedule-find-next').click(function() { freebusy_find_slot(1); });
$('#schedule-freebusy-workinghours').click(function(){
freebusy_ui.workinhoursonly = this.checked;
$('#workinghourscss').remove();
if (this.checked)
$('<style type="text/css" id="workinghourscss"> td.offhours { opacity:0.3; filter:alpha(opacity=30) } </style>').appendTo('head');
});
$('#event-rsvp input.button').click(function(e) {
event_rsvp(this, null, null, e.originalEvent);
});
$('#eventedit input.edit-recurring-savemode').change(function(e) {
var sel = $('input.edit-recurring-savemode:checked').val(),
disabled = sel == 'current' || sel == 'future';
$('#event-panel-recurrence input, #event-panel-recurrence select, #event-panel-attachments input').prop('disabled', disabled);
$('#event-panel-recurrence, #event-panel-attachments')[(disabled?'addClass':'removeClass')]('disabled');
})
$('#eventshow .changersvp').click(function(e) {
var d = $('#eventshow'),
record = $(this).closest('.event-line,.form-group'),
h = d.height() - record.height();
record.toggle();
$('#event-rsvp').slideDown(300, function() {
me.dialog_resize(d.get(0), h + $(this).outerHeight());
if (this.scrollIntoView)
this.scrollIntoView(false);
});
return false;
})
// register click handler for message links
$('#edit-event-links, #event-links').on('click', 'li a.messagelink', function(e) {
rcmail.open_window(this.href);
if (!rcube_event.is_keyboard(e) && this.blur)
this.blur();
return false;
});
// register click handler for message delete buttons
$('#edit-event-links').on('click', 'li a.delete', function(e) {
remove_link(e.target);
return false;
});
$('#agenda-listrange').change(function(e){
settings.agenda_range = parseInt($(this).val());
fc.fullCalendar('changeView', 'list');
// TODO: save new settings in prefs
}).val(settings.agenda_range);
// hide event dialog when clicking somewhere into document
$(document).bind('mousedown', dialog_check);
rcmail.set_busy(false, 'loading', ui_loading);
}
// initialize more UI elements (deferred)
window.setTimeout(init_calendar_ui, exec_deferred);
// fetch counts for some calendars
fetch_counts();
// Save-as-event dialog content
if (rcmail.env.action == 'dialog-ui') {
var date = new Date(), event = {allDay: false, calendar: me.selected_calendar};
date.setHours(date.getHours()+1);
date.setMinutes(0);
var end = new Date(date.getTime());
end.setHours(date.getHours()+1);
event.start = date;
event.end = end;
// exec deferred because it must be after init_calendar_ui
window.setTimeout(function() {
event_edit_dialog('new', $.extend(event, rcmail.env.event_prop));
}, exec_deferred);
rcmail.register_command('event-save', function() { rcmail.env.event_save_func(); }, true);
rcmail.addEventListener('plugin.unlock_saving', function(status) {
me.unlock_saving();
if (status)
window.parent.kolab_event_dialog_element.dialog('destroy');
});
}
} // end rcube_calendar class
// Update layout after initialization
// In devel mode we have to wait until all styles are applied by less
if (rcmail.env.devel_mode && window.less) {
less.pageLoadFinished.then(function() { $(window).resize(); });
}
/* calendar plugin initialization */
window.rcmail && rcmail.addEventListener('init', function(evt) {
// let's go
var cal = new rcube_calendar_ui($.extend(rcmail.env.calendar_settings, rcmail.env.libcal_settings));
if (rcmail.env.action == 'dialog-ui') {
return;
}
// configure toolbar buttons
rcmail.register_command('addevent', function(){ cal.add_event(); });
rcmail.register_command('print', function(){ cal.print_calendars(); }, true);
// configure list operations
rcmail.register_command('calendar-create', function(){ cal.calendar_edit_dialog(null); }, true);
rcmail.register_command('calendar-edit', function(){ cal.calendar_edit_dialog(cal.calendars[cal.selected_calendar]); }, false);
rcmail.register_command('calendar-remove', function(){ cal.calendar_remove(cal.calendars[cal.selected_calendar]); }, false);
rcmail.register_command('calendar-delete', function(){ cal.calendar_delete(cal.calendars[cal.selected_calendar]); }, false);
rcmail.register_command('events-import', function(){ cal.import_events(cal.calendars[cal.selected_calendar]); }, true);
rcmail.register_command('calendar-showurl', function(){ cal.showurl(cal.calendars[cal.selected_calendar]); }, false);
rcmail.register_command('calendar-showfburl', function(){ cal.showfburl(); }, false);
rcmail.register_command('event-download', function(){ cal.event_download(cal.selected_event); }, true);
rcmail.register_command('event-sendbymail', function(p, obj, e){ cal.event_sendbymail(cal.selected_event, e); }, true);
rcmail.register_command('event-copy', function(){ cal.event_copy(cal.selected_event); }, true);
rcmail.register_command('event-history', function(p, obj, e){ cal.event_history_dialog(cal.selected_event); }, false);
rcmail.register_command('talk-room-create', function(){ cal.talk_room_create(); }, true);
// search and export events
rcmail.register_command('export', function(){ cal.export_events(cal.calendars[cal.selected_calendar]); }, true);
rcmail.register_command('search', function(){ cal.quicksearch(); }, true);
rcmail.register_command('reset-search', function(){ cal.reset_quicksearch(); }, true);
// resource invitation dialog
rcmail.register_command('search-resource', function(){ cal.resource_search(); }, true);
rcmail.register_command('reset-resource-search', function(){ cal.reset_resource_search(); }, true);
rcmail.register_command('add-resource', function(){ cal.add_resource2event(); }, false);
// register callback commands
rcmail.addEventListener('plugin.refresh_source', function(data) { cal.calendar_refresh_source(data); });
rcmail.addEventListener('plugin.destroy_source', function(p){ cal.calendar_destroy_source(p.id); });
rcmail.addEventListener('plugin.unlock_saving', function(p){ cal.unlock_saving(); });
rcmail.addEventListener('plugin.refresh_calendar', function(p){ cal.refresh(p); });
rcmail.addEventListener('plugin.import_success', function(p){ cal.import_success(p); });
rcmail.addEventListener('plugin.import_error', function(p){ cal.import_error(p); });
rcmail.addEventListener('plugin.update_counts', function(p){ cal.update_counts(p); });
rcmail.addEventListener('plugin.reload_view', function(p){ cal.reload_view(p); });
rcmail.addEventListener('plugin.resource_data', function(p){ cal.resource_data_load(p); });
rcmail.addEventListener('plugin.resource_owner', function(p){ cal.resource_owner_load(p); });
rcmail.addEventListener('plugin.render_event_changelog', function(data){ cal.render_event_changelog(data); });
rcmail.addEventListener('plugin.event_show_diff', function(data){ cal.event_show_diff(data); });
rcmail.addEventListener('plugin.close_history_dialog', function(data){ cal.close_history_dialog(); });
rcmail.addEventListener('plugin.event_show_revision', function(data){ cal.event_show_dialog(data, null, true); });
rcmail.addEventListener('plugin.itip_message_processed', function(data){ cal.itip_message_processed(data); });
rcmail.addEventListener('plugin.talk_room_created', function(data){ cal.talk_room_created(data); });
rcmail.addEventListener('requestrefresh', function(q){ return cal.before_refresh(q); });
$(window).resize(function(e) {
// check target due to bugs in jquery
// http://bugs.jqueryui.com/ticket/7514
// http://bugs.jquery.com/ticket/9841
if (e.target == window) {
cal.view_resize();
// In Elastic append the datepicker back to sidebar/dialog when resizing
// the window from tablet/phone to desktop and vice-versa
var dp = $('#datepicker'), in_dialog = dp.is('.ui-dialog-content'), width = $(window).width();
if (in_dialog && width > 768) {
if (dp.is(':visible')) {
dp.dialog('close');
}
dp.height('auto').removeClass('ui-dialog-content ui-widget-content')
.data('dialog-parent', dp.closest('.ui-dialog'))
.appendTo('#layout-sidebar');
}
else if (!in_dialog && dp.length && width <= 768 && dp.data('dialog-parent')) {
dp.addClass('ui-dialog-content ui-widget-content')
.insertAfter(dp.data('dialog-parent').find('.ui-dialog-titlebar'));
}
}
}).resize();
// show calendars list when ready
$('#calendars').css('visibility', 'inherit');
// show toolbar
$('#toolbar').show();
// Elastic mods
if ($('#calendar').data('elastic-mode')) {
var selector = $('<div class="btn-group btn-group-toggle" role="group">').appendTo('.fc-header-toolbar > .fc-left'),
nav = $('<div class="btn-group btn-group-toggle" role="group">').appendTo('.fc-header-toolbar > .fc-right');
$('.fc-header-toolbar > .fc-left button').each(function() {
var new_btn, cl = 'btn btn-secondary', btn = $(this),
activate = function(button) {
selector.children('.active').removeClass('active');
$(button).addClass('active');
};
if (btn.is('.fc-state-active')) {
cl += ' active';
}
new_btn = $('<button>').attr({'class': cl, type: 'button'}).text(btn.text())
.appendTo(selector)
.on('click', function() {
activate(this);
btn.click();
});
if (window.MutationObserver) {
// handle button active state changes
new MutationObserver(function() { if (btn.is('.fc-state-active')) activate(new_btn); }).observe(this, {attributes: true});
}
});
$.each(['prev', 'today', 'next'], function() {
var btn = $('.fc-header-toolbar > .fc-right').find('.fc-' + this + '-button');
$('<button>').attr({'class': 'btn btn-secondary ' + this, type: 'button'})
.text(btn.text()).appendTo(nav).on('click', function() { btn.click(); });
});
$('#timezone-display').appendTo($('.fc-header-toolbar > .fc-center')).removeClass('hidden');
$('#agendaoptions').detach().insertAfter('.fc-header-toolbar');
$('.content-frame-navigation a.button.date').appendTo('#layout-content > .searchbar');
// Mobile header title
if (window.MutationObserver) {
var title = $('.fc-header-toolbar > .fc-center h2'),
mobile_header = $('#layout-content > .header > .header-title'),
callback = function() {
var text = title.text();
mobile_header.html('').append([
$('<span class="title">').text(text),
$('<span class="tz">').text($('#timezone-display').text())
]);
};
// update the header when something changes on the calendar title
new MutationObserver(callback).observe(title[0], {childList: true, subtree: true});
// initialize the header
callback();
}
window.calendar_datepicker = function() {
$('#datepicker').dialog({
modal: true,
title: rcmail.gettext('calendar.selectdate'),
buttons: [{
text: rcmail.gettext('close', 'calendar'),
'class': 'cancel',
click: function() { $(this).dialog('close'); }
}],
width: 400,
height: 520
});
}
}
});
diff --git a/plugins/calendar/config.inc.php.dist b/plugins/calendar/config.inc.php.dist
index 5ebb5ec0..14ba761c 100644
--- a/plugins/calendar/config.inc.php.dist
+++ b/plugins/calendar/config.inc.php.dist
@@ -1,155 +1,155 @@
<?php
/*
+-------------------------------------------------------------------------+
| Configuration for the Calendar plugin |
| |
| Copyright (C) 2010, Lazlo Westerhof - Netherlands |
| Copyright (C) 2011-2014, Kolab Systems AG |
| |
| This program is free software: you can redistribute it and/or modify |
| it under the terms of the GNU Affero General Public License as |
| published by the Free Software Foundation, either version 3 of the |
| License, or (at your option) any later version. |
| |
| This program is distributed in the hope that it will be useful, |
| but WITHOUT ANY WARRANTY; without even the implied warranty of |
| MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
| GNU Affero General Public License for more details. |
| |
| You should have received a copy of the GNU Affero General Public License|
| along with this program. If not, see <http://www.gnu.org/licenses/>. |
| |
+-------------------------------------------------------------------------+
| Author: Lazlo Westerhof <hello@lazlo.me> |
| Thomas Bruederli <bruederli@kolabsys.com> |
+-------------------------------------------------------------------------+
*/
-// backend type (database, kolab)
+// backend type (database, kolab, caldav)
$config['calendar_driver'] = "database";
// default calendar view (agendaDay, agendaWeek, month)
$config['calendar_default_view'] = "agendaWeek";
// show a birthdays calendar from the user's address book(s)
$config['calendar_contact_birthdays'] = false;
// timeslots per hour (1, 2, 3, 4, 6)
$config['calendar_timeslots'] = 2;
// show this number of days in agenda view
$config['calendar_agenda_range'] = 60;
// first day of the week (0-6)
$config['calendar_first_day'] = 1;
// first hour of the calendar (0-23)
$config['calendar_first_hour'] = 6;
// working hours begin
$config['calendar_work_start'] = 6;
// working hours end
$config['calendar_work_end'] = 18;
// show line at current time of the day
$config['calendar_time_indicator'] = true;
// Display week numbers:
// -1: don't display week numbers
// 0: in datepicker only (default)
// 1: in both datepicker and calendar
$config['calendar_show_weekno'] = 0;
// default alarm settings for new events.
// this is only a preset when a new event dialog opens
// possible values are <empty>, DISPLAY, EMAIL
$config['calendar_default_alarm_type'] = '';
// default alarm offset for new events.
// use ical-style offset values like "-1H" (one hour before) or "+30M" (30 minutes after)
$config['calendar_default_alarm_offset'] = '-15M';
// how to colorize events:
// 0: according to calendar color
// 1: according to category color
// 2: calendar for outer, category for inner color
// 3: category for outer, calendar for inner color
$config['calendar_event_coloring'] = 0;
// event categories
$config['calendar_categories'] = array(
'Personal' => 'c0c0c0',
'Work' => 'ff0000',
'Family' => '00ff00',
'Holiday' => 'ff6600',
);
// enable users to invite/edit attendees for shared events organized by others
$config['calendar_allow_invite_shared'] = false;
// allow users to accecpt iTip invitations who are no explicitly listed as attendee.
// this can be the case if invitations are sent to mailing lists or alias email addresses.
$config['calendar_allow_itip_uninvited'] = true;
// controls the visibility/default of the checkbox controlling the sending of iTip invitations
// 0 = hidden + disabled
// 1 = hidden + active
// 2 = visible + unchecked
// 3 = visible + active
$config['calendar_itip_send_option'] = 3;
// Action taken after iTip request is handled. Possible values:
// 0 - no action
// 1 - move to Trash
// 2 - delete the message
// 3 - flag as deleted
// folder_name - move the message to the specified folder
$config['calendar_itip_after_action'] = 0;
// enable asynchronous free-busy triggering after data changed
$config['calendar_freebusy_trigger'] = false;
// free-busy information will be displayed for user calendars if available
// 0 - no free-busy information
// 1 - enabled in all views
// 2 - only in quickview
$config['calendar_include_freebusy_data'] = 1;
// SMTP server host used to send (anonymous) itip messages.
// To override the SMTP port or connection method, provide a full URL like 'tls://somehost:587'
// This will add a link to invitation messages to allow users from outside
// to reply when their mail clients do not support iTip format.
$config['calendar_itip_smtp_server'] = null;
// SMTP username used to send (anonymous) itip messages
$config['calendar_itip_smtp_user'] = 'smtpauth';
// SMTP password used to send (anonymous) itip messages
$config['calendar_itip_smtp_pass'] = '123456';
// show virtual invitation calendars (Kolab driver only)
$config['kolab_invitation_calendars'] = false;
// Base URL to build fully qualified URIs to access calendars via CALDAV
// The following replacement variables are supported:
// %h - Current HTTP host
// %u - Current webmail user name
// %n - Calendar name
// %i - Calendar UUID
// $config['calendar_caldav_url'] = 'http://%h/iRony/calendars/%u/%i';
// Driver to provide a resource directory ('ldap' is the only implementation yet).
// Leave empty or commented to disable resources support.
// $config['calendar_resources_driver'] = 'ldap';
// LDAP directory configuration to find avilable resources for events
// $config['calendar_resources_directory'] = array(/* ldap_public-like address book configuration */);
// Enables displaying of free-busy URL with token-based authentication
// Set it to the prefix URL, e.g. 'https://hostname/freebusy' or just '/freebusy'.
// See freebusy_session_auth in configuration of kolab_auth plugin.
$config['calendar_freebusy_session_auth_url'] = null;
// Nextcloud installation URL (for Talk integration).
$config['calendar_nextcloud_url'] = null;
diff --git a/plugins/calendar/drivers/caldav/caldav_calendar.php b/plugins/calendar/drivers/caldav/caldav_calendar.php
index b4e11b2e..89452def 100644
--- a/plugins/calendar/drivers/caldav/caldav_calendar.php
+++ b/plugins/calendar/drivers/caldav/caldav_calendar.php
@@ -1,904 +1,904 @@
<?php
/**
* CalDAV calendar storage class
*
* @author Aleksander Machniak <machniak@apheleia-it.ch>
*
* Copyright (C) 2012-2022, Apheleia IT AG <contact@apheleia-it.ch>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class caldav_calendar extends kolab_storage_dav_folder
{
public $ready = false;
public $rights = 'lrs';
public $editable = false;
public $attachments = false; // TODO
public $alarms = false;
public $history = false;
public $subscriptions = false;
public $categories = [];
public $storage;
public $type = 'event';
protected $cal;
protected $events = [];
protected $search_fields = ['title', 'description', 'location', 'attendees', 'categories'];
/**
* Factory method to instantiate a caldav_calendar object
*
* @param string $id Calendar ID (encoded IMAP folder name)
* @param object $calendar Calendar plugin object
*
* @return caldav_calendar Self instance
*/
public static function factory($id, $calendar)
{
return new caldav_calendar($id, $calendar);
}
/**
* Default constructor
*/
public function __construct($folder_or_id, $calendar)
{
if ($folder_or_id instanceof kolab_storage_dav_folder) {
$this->storage = $folder_or_id;
}
else {
// $this->storage = kolab_storage_dav::get_folder($folder_or_id);
}
$this->cal = $calendar;
$this->id = $this->storage->id;
$this->attributes = $this->storage->attributes;
$this->ready = true;
// Set writeable and alarms flags according to folder permissions
if ($this->ready) {
if ($this->storage->get_namespace() == 'personal') {
$this->editable = true;
$this->rights = 'lrswikxteav';
$this->alarms = true;
}
else {
$rights = $this->storage->get_myrights();
if ($rights && !PEAR::isError($rights)) {
$this->rights = $rights;
if (strpos($rights, 't') !== false || strpos($rights, 'd') !== false) {
$this->editable = strpos($rights, 'i');;
}
}
}
// user-specific alarms settings win
$prefs = $this->cal->rc->config->get('kolab_calendars', []);
if (isset($prefs[$this->id]['showalarms'])) {
$this->alarms = $prefs[$this->id]['showalarms'];
}
}
$this->default = $this->storage->default;
$this->subtype = $this->storage->subtype;
}
/**
* Getter for the folder name
*
* @return string Name of the folder
*/
public function get_realname()
{
return $this->get_name();
}
/**
* Return color to display this calendar
*/
public function get_color($default = null)
{
if ($color = $this->storage->get_color()) {
return $color;
}
return $default ?: 'cc0000';
}
/**
* Compose an URL for CalDAV access to this calendar (if configured)
*/
public function get_caldav_url()
{
/*
if ($template = $this->cal->rc->config->get('calendar_caldav_url', null)) {
return strtr($template, [
'%h' => $_SERVER['HTTP_HOST'],
'%u' => urlencode($this->cal->rc->get_user_name()),
'%i' => urlencode($this->storage->get_uid()),
'%n' => urlencode($this->name),
]);
}
*/
return false;
}
/**
* Update properties of this calendar folder
*
* @see caldav_driver::edit_calendar()
*/
public function update(&$prop)
{
// TODO
return null;
}
/**
* Getter for a single event object
*/
public function get_event($id)
{
// remove our occurrence identifier if it's there
$master_id = preg_replace('/-\d{8}(T\d{6})?$/', '', $id);
// directly access storage object
if (empty($this->events[$id]) && $master_id == $id && ($record = $this->storage->get_object($id))) {
- $this->events[$id] = $this->_to_driver_event($record, true);
+ $this->events[$id] = $record = $this->_to_driver_event($record, true);
}
// maybe a recurring instance is requested
if (empty($this->events[$id]) && $master_id != $id) {
$instance_id = substr($id, strlen($master_id) + 1);
if ($record = $this->storage->get_object($master_id)) {
- $master = $this->_to_driver_event($record);
+ $master = $record = $this->_to_driver_event($record);
}
- if ($master) {
+ if (!empty($master)) {
// check for match in top-level exceptions (aka loose single occurrences)
if (!empty($master['_formatobj']) && ($instance = $master['_formatobj']->get_instance($instance_id))) {
$this->events[$id] = $this->_to_driver_event($instance, false, true, $master);
}
// check for match on the first instance already
else if (!empty($master['_instance']) && $master['_instance'] == $instance_id) {
$this->events[$id] = $master;
}
else if (!empty($master['recurrence'])) {
$start_date = $master['start'];
// For performance reasons we'll get only the specific instance
if (($date = substr($id, strlen($master_id) + 1, 8)) && strlen($date) == 8 && is_numeric($date)) {
$start_date = new DateTime($date . 'T000000', $master['start']->getTimezone());
}
$this->get_recurring_events($record, $start_date, null, $id, 1);
}
}
}
return $this->events[$id];
}
/**
* Get attachment body
* @see calendar_driver::get_attachment_body()
*/
public function get_attachment_body($id, $event)
{
if (!$this->ready) {
return false;
}
$data = $this->storage->get_attachment($event['id'], $id);
if ($data == null) {
// try again with master UID
$uid = preg_replace('/-\d+(T\d{6})?$/', '', $event['id']);
if ($uid != $event['id']) {
$data = $this->storage->get_attachment($uid, $id);
}
}
return $data;
}
/**
* @param int Event's new start (unix timestamp)
* @param int Event's new end (unix timestamp)
* @param string Search query (optional)
* @param bool Include virtual events (optional)
* @param array Additional parameters to query storage
* @param array Additional query to filter events
*
* @return array A list of event records
*/
public function list_events($start, $end, $search = null, $virtual = 1, $query = [], $filter_query = null)
{
// convert to DateTime for comparisons
// #5190: make the range a little bit wider
// to workaround possible timezone differences
try {
$start = new DateTime('@' . ($start - 12 * 3600));
}
catch (Exception $e) {
$start = new DateTime('@0');
}
try {
$end = new DateTime('@' . ($end + 12 * 3600));
}
catch (Exception $e) {
$end = new DateTime('today +10 years');
}
// get email addresses of the current user
$user_emails = $this->cal->get_user_emails();
// query Kolab storage
$query[] = ['dtstart', '<=', $end];
$query[] = ['dtend', '>=', $start];
if (is_array($filter_query)) {
$query = array_merge($query, $filter_query);
}
$words = [];
$partstat_exclude = [];
$events = [];
if (!empty($search)) {
$search = mb_strtolower($search);
$words = rcube_utils::tokenize_string($search, 1);
foreach (rcube_utils::normalize_string($search, true) as $word) {
$query[] = ['words', 'LIKE', $word];
}
}
// set partstat filter to skip pending and declined invitations
if (empty($filter_query)
&& $this->cal->rc->config->get('kolab_invitation_calendars')
&& $this->get_namespace() != 'other'
) {
$partstat_exclude = ['NEEDS-ACTION', 'DECLINED'];
}
foreach ($this->storage->select($query) as $record) {
$event = $this->_to_driver_event($record, !$virtual, false);
// remember seen categories
if (!empty($event['categories'])) {
$cat = is_array($event['categories']) ? $event['categories'][0] : $event['categories'];
$this->categories[$cat]++;
}
// list events in requested time window
if ($event['start'] <= $end && $event['end'] >= $start) {
unset($event['_attendees']);
$add = true;
// skip the first instance of a recurring event if listed in exdate
if ($virtual && !empty($event['recurrence']['EXDATE'])) {
$event_date = $event['start']->format('Ymd');
$event_tz = $event['start']->getTimezone();
foreach ((array) $event['recurrence']['EXDATE'] as $exdate) {
$ex = clone $exdate;
$ex->setTimezone($event_tz);
if ($ex->format('Ymd') == $event_date) {
$add = false;
break;
}
}
}
// find and merge exception for the first instance
if ($virtual && !empty($event['recurrence']) && !empty($event['recurrence']['EXCEPTIONS'])) {
foreach ($event['recurrence']['EXCEPTIONS'] as $exception) {
if ($event['_instance'] == $exception['_instance']) {
unset($exception['calendar'], $exception['className'], $exception['_folder_id']);
// clone date objects from main event before adjusting them with exception data
if (is_object($event['start'])) {
$event['start'] = clone $record['start'];
}
if (is_object($event['end'])) {
$event['end'] = clone $record['end'];
}
kolab_driver::merge_exception_data($event, $exception);
}
}
}
if ($add) {
$events[] = $event;
}
}
// resolve recurring events
if (!empty($event['recurrence']) && $virtual == 1) {
$events = array_merge($events, $this->get_recurring_events($event, $start, $end));
}
// add top-level exceptions (aka loose single occurrences)
else if (!empty($record['exceptions'])) {
foreach ($record['exceptions'] as $ex) {
$component = $this->_to_driver_event($ex, false, false, $record);
if ($component['start'] <= $end && $component['end'] >= $start) {
$events[] = $component;
}
}
}
}
// post-filter all events by fulltext search and partstat values
$me = $this;
$events = array_filter($events, function($event) use ($words, $partstat_exclude, $user_emails, $me) {
// fulltext search
if (count($words)) {
$hits = 0;
foreach ($words as $word) {
$hits += $me->fulltext_match($event, $word, false);
}
if ($hits < count($words)) {
return false;
}
}
// partstat filter
if (count($partstat_exclude) && !empty($event['attendees'])) {
foreach ($event['attendees'] as $attendee) {
if (
in_array($attendee['email'], $user_emails)
&& in_array($attendee['status'], $partstat_exclude)
) {
return false;
}
}
}
return true;
});
// Apply event-to-mail relations
$config = kolab_storage_config::get_instance();
$config->apply_links($events);
// avoid session race conditions that will loose temporary subscriptions
$this->cal->rc->session->nowrite = true;
return $events;
}
/**
* Get number of events in the given calendar
*
* @param int Date range start (unix timestamp)
* @param int Date range end (unix timestamp)
* @param array Additional query to filter events
*
* @return int Number of events
*/
public function count_events($start, $end = null, $filter_query = null)
{
// convert to DateTime for comparisons
try {
$start = new DateTime('@'.$start);
}
catch (Exception $e) {
$start = new DateTime('@0');
}
if ($end) {
try {
$end = new DateTime('@'.$end);
}
catch (Exception $e) {
$end = null;
}
}
// query Kolab storage
$query[] = ['dtend', '>=', $start];
if ($end) {
$query[] = ['dtstart', '<=', $end];
}
// add query to exclude pending/declined invitations
if (empty($filter_query)) {
foreach ($this->cal->get_user_emails() as $email) {
$query[] = ['tags', '!=', 'x-partstat:' . $email . ':needs-action'];
$query[] = ['tags', '!=', 'x-partstat:' . $email . ':declined'];
}
}
else if (is_array($filter_query)) {
$query = array_merge($query, $filter_query);
}
return $this->storage->count($query);
}
/**
* Create a new event record
*
* @see calendar_driver::new_event()
*
* @return array|false The created record ID on success, False on error
*/
public function insert_event($event)
{
if (!is_array($event)) {
return false;
}
// email links are stored separately
$links = !empty($event['links']) ? $event['links'] : [];
unset($event['links']);
// generate new event from RC input
$object = $this->_from_driver_event($event);
$saved = $this->storage->save($object, 'event');
if (!$saved) {
rcube::raise_error([
'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving event object to DAV server"
],
true, false
);
return false;
}
// save links in configuration.relation object
if ($this->save_links($event['uid'], $links)) {
$object['links'] = $links;
}
$this->events = [$event['uid'] => $this->_to_driver_event($object, true)];
return true;
}
/**
* Update a specific event record
*
* @return bool True on success, False on error
*/
public function update_event($event, $exception_id = null)
{
$updated = false;
$old = $this->storage->get_object(!empty($event['uid']) ? $event['uid'] : $event['id']);
if (!$old || PEAR::isError($old)) {
return false;
}
// email links are stored separately
$links = !empty($event['links']) ? $event['links'] : [];
unset($event['links']);
$object = $this->_from_driver_event($event, $old);
$saved = $this->storage->save($object, 'event', $old['uid']);
if (!$saved) {
rcube::raise_error([
'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
'message' => "Error saving event object to CalDAV server"
],
true, false
);
}
else {
// save links in configuration.relation object
if ($this->save_links($event['uid'], $links)) {
$object['links'] = $links;
}
$updated = true;
$this->events = [$event['uid'] => $this->_to_driver_event($object, true)];
// refresh local cache with recurring instances
if ($exception_id) {
$this->get_recurring_events($object, $event['start'], $event['end'], $exception_id);
}
}
return $updated;
}
/**
* Delete an event record
*
* @see calendar_driver::remove_event()
*
* @return bool True on success, False on error
*/
public function delete_event($event, $force = true)
{
$uid = !empty($event['uid']) ? $event['uid'] : $event['id'];
$deleted = $this->storage->delete($uid, $force);
if (!$deleted) {
rcube::raise_error([
'code' => 600, 'file' => __FILE__, 'line' => __LINE__,
'message' => "Error deleting event '{$uid}' from CalDAV server"
],
true, false
);
}
return $deleted;
}
/**
* Restore deleted event record
*
* @see calendar_driver::undelete_event()
*
* @return bool True on success, False on error
*/
public function restore_event($event)
{
// TODO
return false;
}
/**
* Find messages linked with an event
*/
protected function get_links($uid)
{
return []; // TODO
$storage = kolab_storage_config::get_instance();
return $storage->get_object_links($uid);
}
/**
* Save message references (links) to an event
*/
protected function save_links($uid, $links)
{
return false; // TODO
$storage = kolab_storage_config::get_instance();
return $storage->save_object_links($uid, (array) $links);
}
/**
* Create instances of a recurring event
*
* @param array $event Hash array with event properties
* @param DateTime $start Start date of the recurrence window
* @param DateTime $end End date of the recurrence window
* @param string $event_id ID of a specific recurring event instance
* @param int $limit Max. number of instances to return
*
* @return array List of recurring event instances
*/
public function get_recurring_events($event, $start, $end = null, $event_id = null, $limit = null)
{
$object = $event['_formatobj'];
if (!is_object($object)) {
return [];
}
// determine a reasonable end date if none given
if (!$end) {
$end = clone $event['start'];
$end->add(new DateInterval('P100Y'));
}
// read recurrence exceptions first
$events = [];
$exdata = [];
$futuredata = [];
$recurrence_id_format = libcalendaring::recurrence_id_format($event);
if (!empty($event['recurrence'])) {
// copy the recurrence rule from the master event (to be used in the UI)
$recurrence_rule = $event['recurrence'];
unset($recurrence_rule['EXCEPTIONS'], $recurrence_rule['EXDATE']);
if (!empty($event['recurrence']['EXCEPTIONS'])) {
foreach ($event['recurrence']['EXCEPTIONS'] as $exception) {
if (empty($exception['_instance'])) {
$exception['_instance'] = libcalendaring::recurrence_instance_identifier($exception, !empty($event['allday']));
}
$rec_event = $this->_to_driver_event($exception, false, false, $event);
$rec_event['id'] = $event['uid'] . '-' . $exception['_instance'];
$rec_event['isexception'] = 1;
// found the specifically requested instance: register exception (single occurrence wins)
if (
$rec_event['id'] == $event_id
&& (empty($this->events[$event_id]) || !empty($this->events[$event_id]['thisandfuture']))
) {
$rec_event['recurrence'] = $recurrence_rule;
$rec_event['recurrence_id'] = $event['uid'];
$this->events[$rec_event['id']] = $rec_event;
}
// remember this exception's date
$exdate = substr($exception['_instance'], 0, 8);
if (empty($exdata[$exdate]) || !empty($exdata[$exdate]['thisandfuture'])) {
$exdata[$exdate] = $rec_event;
}
if (!empty($rec_event['thisandfuture'])) {
$futuredata[$exdate] = $rec_event;
}
}
}
}
// found the specifically requested instance, exiting...
if ($event_id && !empty($this->events[$event_id])) {
return [$this->events[$event_id]];
}
// Check first occurrence, it might have been moved
if ($first = $exdata[$event['start']->format('Ymd')]) {
// return it only if not already in the result, but in the requested period
if (!($event['start'] <= $end && $event['end'] >= $start)
&& ($first['start'] <= $end && $first['end'] >= $start)
) {
$events[] = $first;
}
}
if ($limit && count($events) >= $limit) {
return $events;
}
// use libkolab to compute recurring events
$recurrence = new kolab_date_recurrence($object);
$i = 0;
while ($next_event = $recurrence->next_instance()) {
$datestr = $next_event['start']->format('Ymd');
$instance_id = $next_event['start']->format($recurrence_id_format);
// use this event data for future recurring instances
if (!empty($futuredata[$datestr])) {
$overlay_data = $futuredata[$datestr];
}
$rec_id = $event['uid'] . '-' . $instance_id;
$exception = !empty($exdata[$datestr]) ? $exdata[$datestr] : $overlay_data;
$event_start = $next_event['start'];
$event_end = $next_event['end'];
// copy some event from exception to get proper start/end dates
if ($exception) {
$event_copy = $next_event;
caldav_driver::merge_exception_dates($event_copy, $exception);
$event_start = $event_copy['start'];
$event_end = $event_copy['end'];
}
// add to output if in range
if (($event_start <= $end && $event_end >= $start) || ($event_id && $rec_id == $event_id)) {
$rec_event = $this->_to_driver_event($next_event, false, false, $event);
$rec_event['_instance'] = $instance_id;
$rec_event['_count'] = $i + 1;
if ($exception) {
// copy data from exception
- colab_driver::merge_exception_data($rec_event, $exception);
+ caldav_driver::merge_exception_data($rec_event, $exception);
}
$rec_event['id'] = $rec_id;
$rec_event['recurrence_id'] = $event['uid'];
$rec_event['recurrence'] = $recurrence_rule;
unset($rec_event['_attendees']);
$events[] = $rec_event;
if ($rec_id == $event_id) {
$this->events[$rec_id] = $rec_event;
break;
}
if ($limit && count($events) >= $limit) {
return $events;
}
}
else if ($next_event['start'] > $end) {
// stop loop if out of range
break;
}
// avoid endless recursion loops
if (++$i > 100000) {
break;
}
}
return $events;
}
/**
* Convert from storage format to internal representation
*/
private function _to_driver_event($record, $noinst = false, $links = true, $master_event = null)
{
$record['calendar'] = $this->id;
// remove (possibly outdated) cached parameters
unset($record['_folder_id'], $record['className']);
if ($links && !array_key_exists('links', $record)) {
$record['links'] = $this->get_links($record['uid']);
}
$ns = $this->get_namespace();
if ($ns == 'other') {
$record['className'] = 'fc-event-ns-other';
}
if ($ns == 'other' || !$this->cal->rc->config->get('kolab_invitation_calendars')) {
$record = caldav_driver::add_partstat_class($record, ['NEEDS-ACTION', 'DECLINED'], $this->get_owner());
// Modify invitation status class name, when invitation calendars are disabled
// we'll use opacity only for declined/needs-action events
$record['className'] = str_replace('-invitation', '', $record['className']);
}
// add instance identifier to first occurrence (master event)
$recurrence_id_format = libcalendaring::recurrence_id_format($master_event ? $master_event : $record);
if (!$noinst && !empty($record['recurrence']) && empty($record['recurrence_id']) && empty($record['_instance'])) {
$record['_instance'] = $record['start']->format($recurrence_id_format);
}
else if (isset($record['recurrence_date']) && is_a($record['recurrence_date'], 'DateTime')) {
$record['_instance'] = $record['recurrence_date']->format($recurrence_id_format);
}
// clean up exception data
if (!empty($record['recurrence']) && !empty($record['recurrence']['EXCEPTIONS'])) {
array_walk($record['recurrence']['EXCEPTIONS'], function(&$exception) {
unset($exception['_mailbox'], $exception['_msguid'], $exception['_formatobj'], $exception['_attachments']);
});
}
// Load the given event data into a libkolabxml container
// it's needed for recurrence resolving, which uses libcalendaring
// TODO: Drop dependency on libkolabxml?
$event_xml = new kolab_format_event();
$event_xml->set($record);
- $event['_formatobj'] = $event_xml;
+ $record['_formatobj'] = $event_xml;
return $record;
}
/**
* Convert the given event record into a data structure that can be passed to the storage backend for saving
* (opposite of self::_to_driver_event())
*/
private function _from_driver_event($event, $old = [])
{
// set current user as ORGANIZER
if ($identity = $this->cal->rc->user->list_emails(true)) {
$event['attendees'] = !empty($event['attendees']) ? $event['attendees'] : [];
$found = false;
// there can be only resources on attendees list (T1484)
// let's check the existence of an organizer
foreach ($event['attendees'] as $attendee) {
if (!empty($attendee['role']) && $attendee['role'] == 'ORGANIZER') {
$found = true;
break;
}
}
if (!$found) {
$event['attendees'][] = ['role' => 'ORGANIZER', 'name' => $identity['name'], 'email' => $identity['email']];
}
$event['_owner'] = $identity['email'];
}
// remove EXDATE values if RDATE is given
if (!empty($event['recurrence']['RDATE'])) {
$event['recurrence']['EXDATE'] = [];
}
// remove recurrence information (e.g. EXDATES and EXCEPTIONS) entirely
if (!empty($event['recurrence']) && empty($event['recurrence']['FREQ']) && empty($event['recurrence']['RDATE'])) {
$event['recurrence'] = [];
}
// keep 'comment' from initial itip invitation
if (!empty($old['comment'])) {
$event['comment'] = $old['comment'];
}
// remove some internal properties which should not be cached
$cleanup_fn = function(&$event) {
unset($event['_savemode'], $event['_fromcalendar'], $event['_identity'], $event['_folder_id'],
$event['calendar'], $event['className'], $event['recurrence_id'],
$event['attachments'], $event['deleted_attachments']);
};
$cleanup_fn($event);
// clean up exception data
if (!empty($event['exceptions'])) {
array_walk($event['exceptions'], function(&$exception) use ($cleanup_fn) {
unset($exception['_mailbox'], $exception['_msguid'], $exception['_formatobj']);
$cleanup_fn($exception);
});
}
// copy meta data (starting with _) from old object
foreach ((array) $old as $key => $val) {
if (!isset($event[$key]) && $key[0] == '_') {
$event[$key] = $val;
}
}
return $event;
}
/**
* Match the given word in the event contents
*/
public function fulltext_match($event, $word, $recursive = true)
{
$hits = 0;
foreach ($this->search_fields as $col) {
if (empty($event[$col])) {
continue;
}
$sval = is_array($event[$col]) ? self::_complex2string($event[$col]) : $event[$col];
if (empty($sval)) {
continue;
}
// do a simple substring matching (to be improved)
$val = mb_strtolower($sval);
if (strpos($val, $word) !== false) {
$hits++;
break;
}
}
return $hits;
}
/**
* Convert a complex event attribute to a string value
*/
private static function _complex2string($prop)
{
static $ignorekeys = ['role', 'status', 'rsvp'];
$out = '';
if (is_array($prop)) {
foreach ($prop as $key => $val) {
if (is_numeric($key)) {
$out .= self::_complex2string($val);
}
else if (!in_array($key, $ignorekeys)) {
$out .= $val . ' ';
}
}
}
else if (is_string($prop) || is_numeric($prop)) {
$out .= $prop . ' ';
}
return rtrim($out);
}
}
diff --git a/plugins/calendar/drivers/caldav/caldav_driver.php b/plugins/calendar/drivers/caldav/caldav_driver.php
index 9e38cc87..6e40c5c9 100644
--- a/plugins/calendar/drivers/caldav/caldav_driver.php
+++ b/plugins/calendar/drivers/caldav/caldav_driver.php
@@ -1,527 +1,530 @@
<?php
/**
* CalDAV driver for the Calendar plugin.
*
* @author Aleksander Machniak <machniak@apheleia-it.ch>
*
* Copyright (C) 2012-2022, Apheleia IT AG <contact@apheleia-it.ch>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
require_once(__DIR__ . '/../kolab/kolab_driver.php');
class caldav_driver extends kolab_driver
{
// features this backend supports
public $alarms = true;
public $attendees = true;
public $freebusy = true;
public $attachments = false; // TODO
public $undelete = false; // TODO
public $alarm_types = ['DISPLAY', 'AUDIO'];
public $categoriesimmutable = true;
/**
* Default constructor
*/
public function __construct($cal)
{
$cal->require_plugin('libkolab');
// load helper classes *after* libkolab has been loaded (#3248)
require_once(__DIR__ . '/caldav_calendar.php');
// require_once(__DIR__ . '/kolab_user_calendar.php');
// require_once(__DIR__ . '/caldav_invitation_calendar.php');
$this->cal = $cal;
$this->rc = $cal->rc;
// Initialize the CalDAV storage
$url = $this->rc->config->get('calendar_caldav_server', 'http://localhost');
$this->storage = new kolab_storage_dav($url);
$this->cal->register_action('push-freebusy', [$this, 'push_freebusy']);
$this->cal->register_action('calendar-acl', [$this, 'calendar_acl']);
// $this->freebusy_trigger = $this->rc->config->get('calendar_freebusy_trigger', false);
// TODO: get configuration for the Bonnie API
// $this->bonnie_api = libkolab::get_bonnie_api();
}
/**
* Read available calendars from server
*/
protected function _read_calendars()
{
// already read sources
if (isset($this->calendars)) {
return $this->calendars;
}
// get all folders that support VEVENT, sorted by namespace/name
$folders = $this->storage->get_folders('event');
// + $this->storage->get_user_folders('event', true);
$this->calendars = [];
foreach ($folders as $folder) {
$calendar = $this->_to_calendar($folder);
if ($calendar->ready) {
$this->calendars[$calendar->id] = $calendar;
if ($calendar->editable) {
$this->has_writeable = true;
}
}
}
return $this->calendars;
}
/**
* Convert kolab_storage_folder into caldav_calendar
*/
protected function _to_calendar($folder)
{
if ($folder instanceof caldav_calendar) {
return $folder;
}
if ($folder instanceof kolab_storage_folder_user) {
$calendar = new kolab_user_calendar($folder, $this->cal);
$calendar->subscriptions = count($folder->children) > 0;
}
else {
$calendar = new caldav_calendar($folder, $this->cal);
}
return $calendar;
}
/**
* Get a list of available calendars from this source.
*
* @param int $filter Bitmask defining filter criterias
* @param object $tree Reference to hierarchical folder tree object
*
* @return array List of calendars
*/
public function list_calendars($filter = 0, &$tree = null)
{
$this->_read_calendars();
$folders = $this->filter_calendars($filter);
$calendars = [];
// include virtual folders for a full folder tree
/*
if (!is_null($tree)) {
$folders = $this->storage->folder_hierarchy($folders, $tree);
}
*/
$parents = array_keys($this->calendars);
foreach ($folders as $id => $cal) {
/*
$path = explode('/', $cal->name);
// find parent
do {
array_pop($path);
$parent_id = $this->storage->folder_id(implode('/', $path));
}
while (count($path) > 1 && !in_array($parent_id, $parents));
// restore "real" parent ID
if ($parent_id && !in_array($parent_id, $parents)) {
$parent_id = $this->storage->folder_id($cal->get_parent());
}
$parents[] = $cal->id;
if ($cal->virtual) {
$calendars[$cal->id] = [
'id' => $cal->id,
'name' => $cal->get_name(),
'listname' => $cal->get_foldername(),
'editname' => $cal->get_foldername(),
'virtual' => true,
'editable' => false,
'group' => $cal->get_namespace(),
];
}
else {
*/
// additional folders may come from kolab_storage_dav::folder_hierarchy() above
// make sure we deal with caldav_calendar instances
$cal = $this->_to_calendar($cal);
$this->calendars[$cal->id] = $cal;
$is_user = ($cal instanceof caldav_user_calendar);
$calendars[$cal->id] = [
'id' => $cal->id,
'name' => $cal->get_name(),
'listname' => $cal->get_foldername(),
'editname' => $cal->get_foldername(),
'title' => '', // $cal->get_title(),
'color' => $cal->get_color(),
'editable' => $cal->editable,
'group' => $is_user ? 'other user' : $cal->get_namespace(),
'active' => $cal->is_active(),
'owner' => $cal->get_owner(),
'removable' => !$cal->default,
+ // extras to hide some elements in the UI
+ 'subscriptions' => false,
+ 'driver' => 'caldav',
];
if (!$is_user) {
$calendars[$cal->id] += [
'default' => $cal->default,
'rights' => $cal->rights,
'showalarms' => $cal->alarms,
'history' => !empty($this->bonnie_api),
'children' => true, // TODO: determine if that folder indeed has child folders
'parent' => $parent_id,
'subtype' => $cal->subtype,
'caldavurl' => '', // $cal->get_caldav_url(),
];
}
/*
}
*/
if ($cal->subscriptions) {
$calendars[$cal->id]['subscribed'] = $cal->is_subscribed();
}
}
/*
// list virtual calendars showing invitations
if ($this->rc->config->get('kolab_invitation_calendars') && !($filter & self::FILTER_INSERTABLE)) {
foreach ([self::INVITATIONS_CALENDAR_PENDING, self::INVITATIONS_CALENDAR_DECLINED] as $id) {
$cal = new caldav_invitation_calendar($id, $this->cal);
if (!($filter & self::FILTER_ACTIVE) || $cal->is_active()) {
$calendars[$id] = [
'id' => $cal->id,
'name' => $cal->get_name(),
'listname' => $cal->get_name(),
'editname' => $cal->get_foldername(),
'title' => $cal->get_title(),
'color' => $cal->get_color(),
'editable' => $cal->editable,
'rights' => $cal->rights,
'showalarms' => $cal->alarms,
'history' => !empty($this->bonnie_api),
'group' => 'x-invitations',
'default' => false,
'active' => $cal->is_active(),
'owner' => $cal->get_owner(),
'children' => false,
'counts' => $id == self::INVITATIONS_CALENDAR_PENDING,
];
if (is_object($tree)) {
$tree->children[] = $cal;
}
}
}
}
*/
// append the virtual birthdays calendar
if ($this->rc->config->get('calendar_contact_birthdays', false) && !($filter & self::FILTER_INSERTABLE)) {
$id = self::BIRTHDAY_CALENDAR_ID;
$prefs = $this->rc->config->get('kolab_calendars', []); // read local prefs
if (!($filter & self::FILTER_ACTIVE) || !empty($prefs[$id]['active'])) {
$calendars[$id] = [
'id' => $id,
'name' => $this->cal->gettext('birthdays'),
'listname' => $this->cal->gettext('birthdays'),
'color' => !empty($prefs[$id]['color']) ? $prefs[$id]['color'] : '87CEFA',
'active' => !empty($prefs[$id]['active']),
'showalarms' => (bool) $this->rc->config->get('calendar_birthdays_alarm_type'),
'group' => 'x-birthdays',
'editable' => false,
'default' => false,
'children' => false,
'history' => false,
];
}
}
return $calendars;
}
/**
* Get the caldav_calendar instance for the given calendar ID
*
* @param string Calendar identifier (encoded imap folder name)
*
* @return ?caldav_calendar Object nor null if calendar doesn't exist
*/
public function get_calendar($id)
{
$this->_read_calendars();
- // create calendar object if necesary
+ // create calendar object if necessary
if (empty($this->calendars[$id])) {
if (in_array($id, [self::INVITATIONS_CALENDAR_PENDING, self::INVITATIONS_CALENDAR_DECLINED])) {
return new caldav_invitation_calendar($id, $this->cal);
}
// for unsubscribed calendar folders
if ($id !== self::BIRTHDAY_CALENDAR_ID) {
$calendar = caldav_calendar::factory($id, $this->cal);
if ($calendar->ready) {
$this->calendars[$calendar->id] = $calendar;
}
}
}
return !empty($this->calendars[$id]) ? $this->calendars[$id] : null;
}
/**
* Search for shared or otherwise not listed calendars the user has access
*
* @param string Search string
* @param string Section/source to search
*
* @return array List of calendars
*/
public function search_calendars($query, $source)
{
$this->calendars = [];
$this->search_more_results = false;
/*
// find unsubscribed IMAP folders that have "event" type
if ($source == 'folders') {
foreach ((array) $this->storage->search_folders('event', $query, ['other']) as $folder) {
$calendar = new kolab_calendar($folder->name, $this->cal);
$this->calendars[$calendar->id] = $calendar;
}
}
// find other user's virtual calendars
else if ($source == 'users') {
// we have slightly more space, so display twice the number
$limit = $this->rc->config->get('autocomplete_max', 15) * 2;
foreach ($this->storage->search_users($query, 0, [], $limit, $count) as $user) {
$calendar = new caldav_user_calendar($user, $this->cal);
$this->calendars[$calendar->id] = $calendar;
// search for calendar folders shared by this user
foreach ($this->storage->list_user_folders($user, 'event', false) as $foldername) {
$cal = new caldav_calendar($foldername, $this->cal);
$this->calendars[$cal->id] = $cal;
$calendar->subscriptions = true;
}
}
if ($count > $limit) {
$this->search_more_results = true;
}
}
// don't list the birthday calendar
$this->rc->config->set('calendar_contact_birthdays', false);
$this->rc->config->set('kolab_invitation_calendars', false);
*/
return $this->list_calendars();
}
/**
* Get events from source.
*
* @param int Event's new start (unix timestamp)
* @param int Event's new end (unix timestamp)
* @param string Search query (optional)
* @param mixed List of calendar IDs to load events from (either as array or comma-separated string)
* @param bool Include virtual events (optional)
* @param int Only list events modified since this time (unix timestamp)
*
* @return array A list of event records
*/
public function load_events($start, $end, $search = null, $calendars = null, $virtual = 1, $modifiedsince = null)
{
if ($calendars && is_string($calendars)) {
$calendars = explode(',', $calendars);
}
else if (!$calendars) {
$this->_read_calendars();
$calendars = array_keys($this->calendars);
}
$query = [];
$events = [];
$categories = [];
if ($modifiedsince) {
$query[] = ['changed', '>=', $modifiedsince];
}
foreach ($calendars as $cid) {
if ($storage = $this->get_calendar($cid)) {
$events = array_merge($events, $storage->list_events($start, $end, $search, $virtual, $query));
$categories += $storage->categories;
}
}
// add events from the address books birthday calendar
if (in_array(self::BIRTHDAY_CALENDAR_ID, $calendars)) {
$events = array_merge($events, $this->load_birthday_events($start, $end, $search, $modifiedsince));
}
// add new categories to user prefs
$old_categories = $this->rc->config->get('calendar_categories', $this->default_categories);
$newcats = array_udiff(
array_keys($categories),
array_keys($old_categories),
function($a, $b) { return strcasecmp($a, $b); }
);
if (!empty($newcats)) {
foreach ($newcats as $category) {
$old_categories[$category] = ''; // no color set yet
}
$this->rc->user->save_prefs(['calendar_categories' => $old_categories]);
}
array_walk($events, 'caldav_driver::to_rcube_event');
return $events;
}
/**
* Create instances of a recurring event
*
* @param array Hash array with event properties
* @param DateTime Start date of the recurrence window
* @param DateTime End date of the recurrence window
*
* @return array List of recurring event instances
*/
public function get_recurring_events($event, $start, $end = null)
{
// load the given event data into a libkolabxml container
$event_xml = new kolab_format_event();
$event_xml->set($event);
$event['_formatobj'] = $event_xml;
$this->_read_calendars();
$storage = reset($this->calendars);
return $storage->get_recurring_events($event, $start, $end);
}
/**
*
*/
protected function get_recurrence_count($event, $dtstart)
{
// load the given event data into a libkolabxml container
$event_xml = new kolab_format_event();
$event_xml->set($event);
$event['_formatobj'] = $event_xml;
// use libkolab to compute recurring events
$recurrence = new kolab_date_recurrence($event['_formatobj']);
$count = 0;
while (($next_event = $recurrence->next_instance()) && $next_event['start'] <= $dtstart && $count < 1000) {
$count++;
}
return $count;
}
/**
* Callback function to produce driver-specific calendar create/edit form
*
* @param string Request action 'form-edit|form-new'
* @param array Calendar properties (e.g. id, color)
* @param array Edit form fields
*
* @return string HTML content of the form
*/
public function calendar_form($action, $calendar, $formfields)
{
$special_calendars = [
self::BIRTHDAY_CALENDAR_ID,
self::INVITATIONS_CALENDAR_PENDING,
self::INVITATIONS_CALENDAR_DECLINED
];
// show default dialog for birthday calendar
if (in_array($calendar['id'], $special_calendars)) {
if ($calendar['id'] != self::BIRTHDAY_CALENDAR_ID) {
unset($formfields['showalarms']);
}
// General tab
$form['props'] = [
'name' => $this->rc->gettext('properties'),
'fields' => $formfields,
];
return kolab_utils::folder_form($form, '', 'calendar');
}
$this->_read_calendars();
if (!empty($calendar['id']) && ($cal = $this->calendars[$calendar['id']])) {
$folder = $cal->get_realname(); // UTF7
$color = $cal->get_color();
}
else {
$folder = '';
$color = '';
}
$hidden_fields[] = ['name' => 'oldname', 'value' => $folder];
$form = [];
$protected = false; // TODO
// Disable folder name input
if ($protected) {
$input_name = new html_hiddenfield(['name' => 'name', 'id' => 'calendar-name']);
$formfields['name']['value'] = $this->storage->object_name($folder)
. $input_name->show($folder);
}
// calendar name (default field)
$form['props']['fields']['location'] = $formfields['name'];
if ($protected) {
// prevent user from moving folder
$hidden_fields[] = ['name' => 'parent', 'value' => '']; // TODO
}
else {
$select = $this->storage->folder_selector('event', ['name' => 'parent', 'id' => 'calendar-parent'], $folder);
$form['props']['fields']['path'] = [
'id' => 'calendar-parent',
'label' => $this->cal->gettext('parentcalendar'),
'value' => $select->show(strlen($folder) ? '' : ''), // TODO
];
}
// calendar color (default field)
$form['props']['fields']['color'] = $formfields['color'];
$form['props']['fields']['alarms'] = $formfields['showalarms'];
return kolab_utils::folder_form($form, $folder, 'calendar', $hidden_fields);
}
}
diff --git a/plugins/calendar/lib/calendar_ui.php b/plugins/calendar/lib/calendar_ui.php
index 25c4cf65..1d4daef2 100644
--- a/plugins/calendar/lib/calendar_ui.php
+++ b/plugins/calendar/lib/calendar_ui.php
@@ -1,1009 +1,1011 @@
<?php
/**
* User Interface class for the Calendar plugin
*
* @author Lazlo Westerhof <hello@lazlo.me>
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2010, Lazlo Westerhof <hello@lazlo.me>
* Copyright (C) 2014, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class calendar_ui
{
private $rc;
private $cal;
private $ready = false;
public $screen;
function __construct($cal)
{
$this->cal = $cal;
$this->rc = $cal->rc;
$this->screen = $this->rc->task == 'calendar' ? ($this->rc->action ?: 'calendar') : 'other';
}
/**
* Calendar UI initialization and requests handlers
*/
public function init()
{
if ($this->ready) {
// already done
return;
}
// add taskbar button
$this->cal->add_button([
'command' => 'calendar',
'class' => 'button-calendar',
'classsel' => 'button-calendar button-selected',
'innerclass' => 'button-inner',
'label' => 'calendar.calendar',
'type' => 'link'
],
'taskbar'
);
// load basic client script
if ($this->rc->action != 'print') {
$this->cal->include_script('calendar_base.js');
}
$this->addCSS();
$this->ready = true;
}
/**
* Register handler methods for the template engine
*/
public function init_templates()
{
$this->cal->register_handler('plugin.calendar_css', [$this, 'calendar_css']);
$this->cal->register_handler('plugin.calendar_list', [$this, 'calendar_list']);
$this->cal->register_handler('plugin.calendar_select', [$this, 'calendar_select']);
$this->cal->register_handler('plugin.identity_select', [$this, 'identity_select']);
$this->cal->register_handler('plugin.category_select', [$this, 'category_select']);
$this->cal->register_handler('plugin.status_select', [$this, 'status_select']);
$this->cal->register_handler('plugin.freebusy_select', [$this, 'freebusy_select']);
$this->cal->register_handler('plugin.priority_select', [$this, 'priority_select']);
$this->cal->register_handler('plugin.alarm_select', [$this, 'alarm_select']);
$this->cal->register_handler('plugin.recurrence_form', [$this->cal->lib, 'recurrence_form']);
$this->cal->register_handler('plugin.attendees_list', [$this, 'attendees_list']);
$this->cal->register_handler('plugin.attendees_form', [$this, 'attendees_form']);
$this->cal->register_handler('plugin.resources_form', [$this, 'resources_form']);
$this->cal->register_handler('plugin.resources_list', [$this, 'resources_list']);
$this->cal->register_handler('plugin.resources_searchform', [$this, 'resources_search_form']);
$this->cal->register_handler('plugin.resource_info', [$this, 'resource_info']);
$this->cal->register_handler('plugin.resource_calendar', [$this, 'resource_calendar']);
$this->cal->register_handler('plugin.attendees_freebusy_table', [$this, 'attendees_freebusy_table']);
$this->cal->register_handler('plugin.edit_attendees_notify', [$this, 'edit_attendees_notify']);
$this->cal->register_handler('plugin.edit_recurrence_sync', [$this, 'edit_recurrence_sync']);
$this->cal->register_handler('plugin.edit_recurring_warning', [$this, 'recurring_event_warning']);
$this->cal->register_handler('plugin.event_rsvp_buttons', [$this, 'event_rsvp_buttons']);
$this->cal->register_handler('plugin.agenda_options', [$this, 'agenda_options']);
$this->cal->register_handler('plugin.events_import_form', [$this, 'events_import_form']);
$this->cal->register_handler('plugin.events_export_form', [$this, 'events_export_form']);
$this->cal->register_handler('plugin.object_changelog_table', ['libkolab', 'object_changelog_table']);
$this->cal->register_handler('plugin.searchform', [$this->rc->output, 'search_form']);
kolab_attachments_handler::ui();
}
/**
* Adds CSS stylesheets to the page header
*/
public function addCSS()
{
$skin_path = $this->cal->local_skin_path();
if (
$this->rc->task == 'calendar'
&& (!$this->rc->action || in_array($this->rc->action, ['index', 'print']))
) {
// Include fullCalendar style before skin file for simpler style overriding
$this->cal->include_stylesheet($skin_path . '/fullcalendar.css');
}
$this->cal->include_stylesheet($skin_path . '/calendar.css');
if ($this->rc->task == 'calendar' && $this->rc->action == 'print') {
$this->cal->include_stylesheet($skin_path . '/print.css');
}
}
/**
* Adds JS files to the page header
*/
public function addJS()
{
$this->cal->include_script('lib/js/moment.js');
$this->cal->include_script('lib/js/fullcalendar.js');
if ($this->rc->task == 'calendar' && $this->rc->action == 'print') {
$this->cal->include_script('print.js');
}
else {
$this->rc->output->include_script('treelist.js');
$this->cal->api->include_script('libkolab/libkolab.js');
$this->cal->include_script('calendar_ui.js');
jqueryui::miniColors();
}
}
/**
* Add custom style for the calendar UI
*/
function calendar_css($attrib = [])
{
$categories = $this->cal->driver->list_categories();
$calendars = $this->cal->driver->list_calendars();
$js_categories = [];
$mode = $this->rc->config->get('calendar_event_coloring', $this->cal->defaults['calendar_event_coloring']);
$css = "\n";
foreach ((array) $categories as $class => $color) {
if (!empty($color)) {
$js_categories[$class] = $color;
$color = ltrim($color, '#');
$class = 'cat-' . asciiwords(strtolower($class), true);
$css .= ".$class { color: #$color; }\n";
}
}
$this->rc->output->set_env('calendar_categories', $js_categories);
foreach ((array) $calendars as $id => $prop) {
if (!empty($prop['color'])) {
$css .= $this->calendar_css_classes($id, $prop, $mode, $attrib);
}
}
return html::tag('style', ['type' => 'text/css'], $css);
}
/**
* Calendar folder specific CSS classes
*/
public function calendar_css_classes($id, $prop, $mode, $attrib = [])
{
$color = $folder_color = $prop['color'];
// replace white with skin-defined color
if (!empty($attrib['folder-fallback-color']) && preg_match('/^f+$/i', $folder_color)) {
$folder_color = ltrim($attrib['folder-fallback-color'], '#');
}
$class = 'cal-' . asciiwords($id, true);
$css = "li .$class";
if (!empty($attrib['folder-class'])) {
$css = str_replace('$class', $class, $attrib['folder-class']);
}
$css .= " { color: #$folder_color; }\n";
return $css . ".$class .handle { background-color: #$color; }\n";
}
/**
* Generate HTML content of the calendars list (or metadata only)
*/
function calendar_list($attrib = [], $js_only = false)
{
$html = '';
$jsenv = [];
$tree = true;
$calendars = $this->cal->driver->list_calendars(0, $tree);
// walk folder tree
if (is_object($tree)) {
$html = $this->list_tree_html($tree, $calendars, $jsenv, $attrib);
// append birthdays calendar which isn't part of $tree
if (!empty($calendars[calendar_driver::BIRTHDAY_CALENDAR_ID])) {
$bdaycal = $calendars[calendar_driver::BIRTHDAY_CALENDAR_ID];
$calendars = [calendar_driver::BIRTHDAY_CALENDAR_ID => $bdaycal];
}
else {
$calendars = []; // clear array for flat listing
}
}
else if (isset($attrib['class'])) {
// fall-back to flat folder listing
$attrib['class'] .= ' flat';
}
foreach ((array) $calendars as $id => $prop) {
if (!empty($attrib['activeonly']) && empty($prop['active'])) {
continue;
}
$li_content = $this->calendar_list_item($id, $prop, $jsenv, !empty($attrib['activeonly']));
$li_attr = [
'id' => 'rcmlical' . $id,
'class' => isset($prop['group']) ? $prop['group'] : null,
];
$html .= html::tag('li', $li_attr, $li_content);
}
$this->rc->output->set_env('calendars', $jsenv);
if ($js_only) {
return;
}
$this->rc->output->set_env('source', rcube_utils::get_input_value('source', rcube_utils::INPUT_GET));
$this->rc->output->add_gui_object('calendarslist', !empty($attrib['id']) ? $attrib['id'] : 'rccalendarlist');
return html::tag('ul', $attrib, $html, html::$common_attrib);
}
/**
* Return html for a structured list <ul> for the folder tree
*/
public function list_tree_html($node, $data, &$jsenv, $attrib)
{
$out = '';
foreach ($node->children as $folder) {
$id = $folder->id;
$prop = $data[$id];
$is_collapsed = false; // TODO: determine this somehow?
$content = $this->calendar_list_item($id, $prop, $jsenv, !empty($attrib['activeonly']));
if (!empty($folder->children)) {
$content .= html::tag('ul', ['style' => $is_collapsed ? "display:none;" : null],
$this->list_tree_html($folder, $data, $jsenv, $attrib)
);
}
if (strlen($content)) {
$li_attr = [
'id' => 'rcmlical' . rcube_utils::html_identifier($id),
'class' => $prop['group'] . (!empty($prop['virtual']) ? ' virtual' : ''),
];
$out .= html::tag('li', $li_attr, $content);
}
}
return $out;
}
/**
* Helper method to build a calendar list item (HTML content and js data)
*/
public function calendar_list_item($id, $prop, &$jsenv, $activeonly = false)
{
// enrich calendar properties with settings from the driver
if (empty($prop['virtual'])) {
unset($prop['user_id']);
$prop['alarms'] = $this->cal->driver->alarms;
$prop['attendees'] = $this->cal->driver->attendees;
$prop['freebusy'] = $this->cal->driver->freebusy;
$prop['attachments'] = $this->cal->driver->attachments;
$prop['undelete'] = $this->cal->driver->undelete;
$prop['feedurl'] = $this->cal->get_url([
'_cal' => $this->cal->ical_feed_hash($id) . '.ics',
'action' => 'feed'
]
);
$jsenv[$id] = $prop;
}
if (!empty($prop['title'])) {
$title = $prop['title'];
}
else if ($prop['name'] != $prop['listname'] || strlen($prop['name']) > 25) {
$title = html_entity_decode($prop['name'], ENT_COMPAT, RCUBE_CHARSET);
}
else {
$title = '';
}
$classes = ['calendar', 'cal-' . asciiwords($id, true)];
if (!empty($prop['virtual'])) {
$classes[] = 'virtual';
}
else if (empty($prop['editable'])) {
$classes[] = 'readonly';
}
if (!empty($prop['subscribed'])) {
$classes[] = 'subscribed';
if ($prop['subscribed'] === 2) {
$classes[] = 'partial';
}
}
if (!empty($prop['class'])) {
$classes[] = $prop['class'];
}
$content = '';
if (!$activeonly || !empty($prop['active'])) {
$label_id = 'cl:' . $id;
$content = html::a(
['class' => 'calname', 'id' => $label_id, 'title' => $title, 'href' => '#'],
rcube::Q(!empty($prop['editname']) ? $prop['editname'] : $prop['listname'])
);
if (empty($prop['virtual'])) {
$color = !empty($prop['color']) ? $prop['color'] : 'f00';
$actions = '';
if (!EMPTY($prop['removable'])) {
$actions .= html::a([
'href' => '#',
'class' => 'remove',
'title' => $this->cal->gettext('removelist')
], ' '
);
}
$actions .= html::a([
'href' => '#',
'class' => 'quickview',
'title' => $this->cal->gettext('quickview'),
'role' => 'checkbox',
'aria-checked' => 'false'
], ''
);
if (!empty($prop['subscribed'])) {
$actions .= html::a([
'href' => '#',
'class' => 'subscribed',
'title' => $this->cal->gettext('calendarsubscribe'),
'role' => 'checkbox',
'aria-checked' => !empty($prop['subscribed']) ? 'true' : 'false'
], ' '
);
}
- $content .= html::tag('input', [
- 'type' => 'checkbox',
- 'name' => '_cal[]',
- 'value' => $id,
- 'checked' => !empty($prop['active']),
- 'aria-labelledby' => $label_id
- ])
- . html::span('actions', $actions)
- . html::span(['class' => 'handle', 'style' => "background-color: #$color"], ' ');
+ if (!isset($prop['subscriptions']) || $prop['subscriptions'] !== false) {
+ $content .= html::tag('input', [
+ 'type' => 'checkbox',
+ 'name' => '_cal[]',
+ 'value' => $id,
+ 'checked' => !empty($prop['active']),
+ 'aria-labelledby' => $label_id
+ ])
+ . html::span('actions', $actions)
+ . html::span(['class' => 'handle', 'style' => "background-color: #$color"], ' ');
+ }
}
$content = html::div(join(' ', $classes), $content);
}
return $content;
}
/**
* Render a HTML for agenda options form
*/
function agenda_options($attrib = [])
{
$attrib += ['id' => 'agendaoptions'];
$attrib['style'] = 'display:none';
$select_range = new html_select(['name' => 'listrange', 'id' => 'agenda-listrange', 'class' => 'form-control custom-select']);
$select_range->add(1 . ' ' . preg_replace('/\(.+\)/', '', $this->cal->lib->gettext('days')), '');
foreach ([2,5,7,14,30,60,90,180,365] as $days) {
$select_range->add($days . ' ' . preg_replace('/\(|\)/', '', $this->cal->lib->gettext('days')), $days);
}
$html = html::span('input-group',
html::label(['for' => 'agenda-listrange', 'class' => 'input-group-prepend'],
html::span('input-group-text', $this->cal->gettext('listrange'))
)
. $select_range->show($this->rc->config->get('calendar_agenda_range', $this->cal->defaults['calendar_agenda_range']))
);
return html::div($attrib, $html);
}
/**
* Render a HTML select box for calendar selection
*/
function calendar_select($attrib = [])
{
$attrib['name'] = 'calendar';
$attrib['is_escaped'] = true;
$select = new html_select($attrib);
foreach ((array) $this->cal->driver->list_calendars() as $id => $prop) {
if (
!empty($prop['editable'])
|| (!empty($prop['rights']) && strpos($prop['rights'], 'i') !== false)
) {
$select->add($prop['name'], $id);
}
}
return $select->show(null);
}
/**
* Render a HTML select box for user identity selection
*/
function identity_select($attrib = [])
{
$attrib['name'] = 'identity';
$select = new html_select($attrib);
$identities = $this->rc->user->list_emails();
foreach ($identities as $ident) {
$select->add(format_email_recipient($ident['email'], $ident['name']), $ident['identity_id']);
}
return $select->show(null);
}
/**
* Render a HTML select box to select an event category
*/
function category_select($attrib = [])
{
$attrib['name'] = 'categories';
$select = new html_select($attrib);
$select->add('---', '');
foreach (array_keys((array) $this->cal->driver->list_categories()) as $cat) {
$select->add($cat, $cat);
}
return $select->show(null);
}
/**
* Render a HTML select box for status property
*/
function status_select($attrib = [])
{
$attrib['name'] = 'status';
$select = new html_select($attrib);
$select->add('---', '');
$select->add($this->cal->gettext('status-confirmed'), 'CONFIRMED');
$select->add($this->cal->gettext('status-cancelled'), 'CANCELLED');
$select->add($this->cal->gettext('status-tentative'), 'TENTATIVE');
return $select->show(null);
}
/**
* Render a HTML select box for free/busy/out-of-office property
*/
function freebusy_select($attrib = [])
{
$attrib['name'] = 'freebusy';
$select = new html_select($attrib);
$select->add($this->cal->gettext('free'), 'free');
$select->add($this->cal->gettext('busy'), 'busy');
// out-of-office is not supported by libkolabxml (#3220)
// $select->add($this->cal->gettext('outofoffice'), 'outofoffice');
$select->add($this->cal->gettext('tentative'), 'tentative');
return $select->show(null);
}
/**
* Render a HTML select for event priorities
*/
function priority_select($attrib = [])
{
$attrib['name'] = 'priority';
$select = new html_select($attrib);
$select->add('---', '0');
$select->add('1 ' . $this->cal->gettext('highest'), '1');
$select->add('2 ' . $this->cal->gettext('high'), '2');
$select->add('3 ', '3');
$select->add('4 ', '4');
$select->add('5 ' . $this->cal->gettext('normal'), '5');
$select->add('6 ', '6');
$select->add('7 ', '7');
$select->add('8 ' . $this->cal->gettext('low'), '8');
$select->add('9 ' . $this->cal->gettext('lowest'), '9');
return $select->show(null);
}
/**
* Render HTML form for alarm configuration
*/
function alarm_select($attrib = [])
{
return $this->cal->lib->alarm_select($attrib, $this->cal->driver->alarm_types, $this->cal->driver->alarm_absolute);
}
/**
* Render HTML for attendee notification warning
*/
function edit_attendees_notify($attrib = [])
{
$checkbox = new html_checkbox(['name' => '_notify', 'id' => 'edit-attendees-donotify', 'value' => 1, 'class' => 'pretty-checkbox']);
return html::div($attrib, html::label(null, $checkbox->show(1) . ' ' . $this->cal->gettext('sendnotifications')));
}
/**
* Render HTML for recurrence option to align start date with the recurrence rule
*/
function edit_recurrence_sync($attrib = [])
{
$checkbox = new html_checkbox(['name' => '_start_sync', 'value' => 1, 'class' => 'pretty-checkbox']);
return html::div($attrib, html::label(null, $checkbox->show(1) . ' ' . $this->cal->gettext('eventstartsync')));
}
/**
* Generate the form for recurrence settings
*/
function recurring_event_warning($attrib = [])
{
$attrib['id'] = 'edit-recurring-warning';
$radio = new html_radiobutton(['name' => '_savemode', 'class' => 'edit-recurring-savemode']);
$form = html::label(null, $radio->show('', ['value' => 'current']) . $this->cal->gettext('currentevent')) . ' '
. html::label(null, $radio->show('', ['value' => 'future']) . $this->cal->gettext('futurevents')) . ' '
. html::label(null, $radio->show('all', ['value' => 'all']) . $this->cal->gettext('allevents')) . ' '
. html::label(null, $radio->show('', ['value' => 'new']) . $this->cal->gettext('saveasnew'));
return html::div($attrib,
html::div('message', $this->cal->gettext('changerecurringeventwarning'))
. html::div('savemode', $form)
);
}
/**
* Form for uploading and importing events
*/
function events_import_form($attrib = [])
{
if (empty($attrib['id'])) {
$attrib['id'] = 'rcmImportForm';
}
// Get max filesize, enable upload progress bar
$max_filesize = $this->rc->upload_init();
$accept = '.ics, text/calendar, text/x-vcalendar, application/ics';
if (class_exists('ZipArchive', false)) {
$accept .= ', .zip, application/zip';
}
$input = new html_inputfield([
'id' => 'importfile',
'type' => 'file',
'name' => '_data',
'size' => !empty($attrib['uploadfieldsize']) ? $attrib['uploadfieldsize'] : null,
'accept' => $accept
]);
$select = new html_select(['name' => '_range', 'id' => 'event-import-range']);
$select->add([
$this->cal->gettext('onemonthback'),
$this->cal->gettext(['name' => 'nmonthsback', 'vars' => ['nr'=>2]]),
$this->cal->gettext(['name' => 'nmonthsback', 'vars' => ['nr'=>3]]),
$this->cal->gettext(['name' => 'nmonthsback', 'vars' => ['nr'=>6]]),
$this->cal->gettext(['name' => 'nmonthsback', 'vars' => ['nr'=>12]]),
$this->cal->gettext('all'),
],
['1','2','3','6','12',0]
);
$html = html::div('form-section form-group row',
html::label(['class' => 'col-sm-4 col-form-label', 'for' => 'importfile'],
rcube::Q($this->rc->gettext('importfromfile'))
)
. html::div('col-sm-8', $input->show()
. html::div('hint', $this->rc->gettext(['name' => 'maxuploadsize', 'vars' => ['size' => $max_filesize]]))
)
);
$html .= html::div('form-section form-group row',
html::label(['for' => 'event-import-calendar', 'class' => 'col-form-label col-sm-4'],
$this->cal->gettext('calendar')
)
. html::div('col-sm-8', $this->calendar_select(['name' => 'calendar', 'id' => 'event-import-calendar']))
);
$html .= html::div('form-section form-group row',
html::label(['for' => 'event-import-range', 'class' => 'col-form-label col-sm-4'],
$this->cal->gettext('importrange')
)
. html::div('col-sm-8', $select->show(1))
);
$this->rc->output->add_gui_object('importform', $attrib['id']);
$this->rc->output->add_label('import');
return html::tag('p', null, $this->cal->gettext('importtext'))
. html::tag('form', [
'action' => $this->rc->url(['task' => 'calendar', 'action' => 'import_events']),
'method' => 'post',
'enctype' => 'multipart/form-data',
'id' => $attrib['id']
], $html
);
}
/**
* Form to select options for exporting events
*/
function events_export_form($attrib = [])
{
if (empty($attrib['id'])) {
$attrib['id'] = 'rcmExportForm';
}
$html = html::div('form-section form-group row',
html::label(['for' => 'event-export-calendar', 'class' => 'col-sm-4 col-form-label'],
$this->cal->gettext('calendar')
)
. html::div('col-sm-8', $this->calendar_select(['name' => 'calendar', 'id' => 'event-export-calendar', 'class' => 'form-control custom-select']))
);
$select = new html_select([
'name' => 'range',
'id' => 'event-export-range',
'class' => 'form-control custom-select rounded-right'
]);
$select->add([
$this->cal->gettext('all'),
$this->cal->gettext('onemonthback'),
$this->cal->gettext(['name' => 'nmonthsback', 'vars' => ['nr' => 2]]),
$this->cal->gettext(['name' => 'nmonthsback', 'vars' => ['nr' => 3]]),
$this->cal->gettext(['name' => 'nmonthsback', 'vars' => ['nr' => 6]]),
$this->cal->gettext(['name' => 'nmonthsback', 'vars' => ['nr' => 12]]),
$this->cal->gettext('customdate'),
],
[0,'1','2','3','6','12','custom']
);
$startdate = new html_inputfield([
'name' => 'start',
'size' => 11,
'id' => 'event-export-startdate',
'style' => 'display:none'
]);
$html .= html::div('form-section form-group row',
html::label(['for' => 'event-export-range', 'class' => 'col-sm-4 col-form-label'],
$this->cal->gettext('exportrange')
)
. html::div('col-sm-8 input-group', $select->show(0) . $startdate->show())
);
$checkbox = new html_checkbox([
'name' => 'attachments',
'id' => 'event-export-attachments',
'value' => 1,
'class' => 'form-check-input pretty-checkbox'
]);
$html .= html::div('form-section form-check row',
html::label(['for' => 'event-export-attachments', 'class' => 'col-sm-4 col-form-label'],
$this->cal->gettext('exportattachments')
)
. html::div('col-sm-8', $checkbox->show(1))
);
$this->rc->output->add_gui_object('exportform', $attrib['id']);
return html::tag('form', $attrib + [
'action' => $this->rc->url(['task' => 'calendar', 'action' => 'export_events']),
'method' => 'post',
'id' => $attrib['id']
],
$html
);
}
/**
* Handler for calendar form template.
* The form content could be overriden by the driver
*/
function calendar_editform($action, $calendar = [])
{
$this->action = $action;
$this->calendar = $calendar;
// load miniColors js/css files
jqueryui::miniColors();
$this->rc->output->set_env('pagetitle', $this->cal->gettext('calendarprops'));
$this->rc->output->add_handler('folderform', [$this, 'calendarform']);
$this->rc->output->send('libkolab.folderform');
}
/**
* Handler for calendar form template.
* The form content could be overriden by the driver
*/
function calendarform($attrib)
{
// compose default calendar form fields
$input_name = new html_inputfield(['name' => 'name', 'id' => 'calendar-name', 'size' => 20]);
$input_color = new html_inputfield(['name' => 'color', 'id' => 'calendar-color', 'size' => 7, 'class' => 'colors']);
$formfields = [
'name' => [
'label' => $this->cal->gettext('name'),
'value' => $input_name->show(isset($this->calendar['name']) ? $this->calendar['name'] : ''),
'id' => 'calendar-name',
],
'color' => [
'label' => $this->cal->gettext('color'),
'value' => $input_color->show(isset($this->calendar['color']) ? $this->calendar['color'] : ''),
'id' => 'calendar-color',
],
];
if (!empty($this->cal->driver->alarms)) {
$checkbox = new html_checkbox(['name' => 'showalarms', 'id' => 'calendar-showalarms', 'value' => 1]);
$formfields['showalarms'] = [
'label' => $this->cal->gettext('showalarms'),
'value' => $checkbox->show(!empty($this->calendar['showalarms']) ? 1 : 0),
'id' => 'calendar-showalarms',
];
}
// allow driver to extend or replace the form content
return html::tag('form', $attrib + ['action' => '#', 'method' => 'get', 'id' => 'calendarpropform'],
$this->cal->driver->calendar_form($this->action, $this->calendar, $formfields)
);
}
/**
* Render HTML for attendees table
*/
function attendees_list($attrib = [])
{
// add "noreply" checkbox to attendees table only
$invitations = strpos($attrib['id'], 'attend') !== false;
$invite = new html_checkbox(['value' => 1, 'id' => 'edit-attendees-invite']);
$table = new html_table(['cols' => 5 + intval($invitations), 'border' => 0, 'cellpadding' => 0, 'class' => 'rectable']);
$table->add_header('role', $this->cal->gettext('role'));
$table->add_header('name', $this->cal->gettext(!empty($attrib['coltitle']) ? $attrib['coltitle'] : 'attendee'));
$table->add_header('availability', $this->cal->gettext('availability'));
$table->add_header('confirmstate', $this->cal->gettext('confirmstate'));
if ($invitations) {
$table->add_header(['class' => 'invite', 'title' => $this->cal->gettext('sendinvitations')],
$invite->show(1)
. html::label('edit-attendees-invite', html::span('inner', $this->cal->gettext('sendinvitations')))
);
}
$table->add_header('options', '');
// hide invite column if disabled by config
$itip_notify = (int)$this->rc->config->get('calendar_itip_send_option', $this->cal->defaults['calendar_itip_send_option']);
if ($invitations && !($itip_notify & 2)) {
$css = sprintf('#%s td.invite, #%s th.invite { display:none !important }', $attrib['id'], $attrib['id']);
$this->rc->output->add_footer(html::tag('style', ['type' => 'text/css'], $css));
}
return $table->show($attrib);
}
/**
* Render HTML for attendees adding form
*/
function attendees_form($attrib = [])
{
$input = new html_inputfield([
'name' => 'participant',
'id' => 'edit-attendee-name',
'class' => 'form-control'
]);
$textarea = new html_textarea([
'name' => 'comment',
'id' => 'edit-attendees-comment',
'class' => 'form-control',
'rows' => 4,
'cols' => 55,
'title' => $this->cal->gettext('itipcommenttitle')
]);
return html::div($attrib,
html::div('form-searchbar',
$input->show()
. ' ' .
html::tag('input', [
'type' => 'button',
'class' => 'button',
'id' => 'edit-attendee-add',
'value' => $this->cal->gettext('addattendee')
])
. ' ' .
html::tag('input', [
'type' => 'button',
'class' => 'button',
'id' => 'edit-attendee-schedule',
'value' => $this->cal->gettext('scheduletime') . '...'
])
)
. html::p('attendees-commentbox', html::label('edit-attendees-comment', $this->cal->gettext('itipcomment')) . $textarea->show())
);
}
/**
* Render HTML for resources adding form
*/
function resources_form($attrib = [])
{
$input = new html_inputfield(['name' => 'resource', 'id' => 'edit-resource-name', 'class' => 'form-control']);
return html::div($attrib,
html::div('form-searchbar',
$input->show()
. ' ' .
html::tag('input', [
'type' => 'button',
'class' => 'button',
'id' => 'edit-resource-add',
'value' => $this->cal->gettext('addresource')
])
. ' ' .
html::tag('input', [
'type' => 'button',
'class' => 'button',
'id' => 'edit-resource-find',
'value' => $this->cal->gettext('findresources') . '...'
])
)
);
}
/**
* Render HTML for resources list
*/
function resources_list($attrib = [])
{
$attrib += ['id' => 'calendar-resources-list'];
$this->rc->output->add_gui_object('resourceslist', $attrib['id']);
return html::tag('ul', $attrib, '', html::$common_attrib);
}
/**
*
*/
public function resource_info($attrib = [])
{
$attrib += ['id' => 'calendar-resources-info'];
$this->rc->output->add_gui_object('resourceinfo', $attrib['id']);
$this->rc->output->add_gui_object('resourceownerinfo', $attrib['id'] . '-owner');
// copy address book labels for owner details to client
$this->rc->output->add_label('name','firstname','surname','department','jobtitle','email','phone','address');
$table_attrib = ['id','class','style','width','summary','cellpadding','cellspacing','border'];
return html::tag('table', $attrib, html::tag('tbody', null, ''), $table_attrib)
. html::tag('table', ['id' => $attrib['id'] . '-owner', 'style' => 'display:none'] + $attrib,
html::tag('thead', null,
html::tag('tr', null,
html::tag('td', ['colspan' => 2], rcube::Q($this->cal->gettext('resourceowner')))
)
)
. html::tag('tbody', null, ''),
$table_attrib
);
}
/**
*
*/
public function resource_calendar($attrib = [])
{
$attrib += ['id' => 'calendar-resources-calendar'];
$this->rc->output->add_gui_object('resourceinfocalendar', $attrib['id']);
return html::div($attrib, '');
}
/**
* GUI object 'searchform' for the resource finder dialog
*
* @param array $attrib Named parameters
*
* @return string HTML code for the gui object
*/
function resources_search_form($attrib)
{
$attrib += [
'command' => 'search-resource',
'reset-command' => 'reset-resource-search',
'id' => 'rcmcalresqsearchbox',
'autocomplete' => 'off',
'form-name' => 'rcmcalresoursqsearchform',
'gui-object' => 'resourcesearchform',
];
// add form tag around text field
return $this->rc->output->search_form($attrib);
}
/**
*
*/
function attendees_freebusy_table($attrib = [])
{
$table = new html_table(['cols' => 2, 'border' => 0, 'cellspacing' => 0]);
$table->add('attendees',
html::tag('h3', 'boxtitle', $this->cal->gettext('tabattendees'))
. html::div('timesheader', ' ')
. html::div(['id' => 'schedule-attendees-list', 'class' => 'attendees-list'], '')
);
$table->add('times',
html::div('scroll',
html::tag('table', ['id' => 'schedule-freebusy-times', 'border' => 0, 'cellspacing' => 0],
html::tag('thead') . html::tag('tbody')
)
. html::div(['id' => 'schedule-event-time', 'style' => 'display:none'], ' ')
)
);
return $table->show($attrib);
}
/**
*
*/
function event_invitebox($attrib = [])
{
if (!empty($this->cal->event)) {
return html::div($attrib,
$this->cal->itip->itip_object_details_table($this->cal->event, $this->cal->itip->gettext('itipinvitation'))
. $this->cal->invitestatus
);
}
return '';
}
function event_rsvp_buttons($attrib = [])
{
$actions = ['accepted', 'tentative', 'declined'];
if (empty($attrib['delegate']) || $attrib['delegate'] !== 'false') {
$actions[] = 'delegated';
}
return $this->cal->itip->itip_rsvp_buttons($attrib, $actions);
}
}
diff --git a/plugins/calendar/skins/elastic/templates/calendar.html b/plugins/calendar/skins/elastic/templates/calendar.html
index 06aad50b..3331a521 100644
--- a/plugins/calendar/skins/elastic/templates/calendar.html
+++ b/plugins/calendar/skins/elastic/templates/calendar.html
@@ -1,379 +1,381 @@
<roundcube:include file="includes/layout.html" />
<roundcube:include file="includes/menu.html" />
<h1 class="voice"><roundcube:label name="calendar.calendar" /></h1>
<!-- calendars list -->
<div id="layout-sidebar" class="listbox" role="navigation" aria-labelledby="arial-label-calendars">
<div class="header">
<a class="button icon back-content-button" href="#back" data-hidden="big"><span class="inner"><roundcube:label name="back" /></span></a>
<span id="aria-label-calendars" class="header-title"><roundcube:label name="calendar.calendars" /></span>
<roundcube:button name="calendaractionsmenu" id="calendaroptionsmenulink" type="link"
title="calendar.calendaractions" class="button icon sidebar-menu" data-popup="calendaractions-menu"
innerClass="inner" label="actions" />
</div>
<roundcube:object name="libkolab.folder_search_form" id="calendarlistsearch" wrapper="searchbar menu"
ariatag="h2" label="calsearchform" label-domain="calendar" buttontitle="findcalendars" />
<div id="calendars-content" class="scroller">
<roundcube:object name="plugin.calendar_list" id="calendarslist" class="treelist listing iconized" />
</div>
<h2 id="aria-label-minical" class="voice"><roundcube:label name="calendar.arialabelminical" /></h2>
<div id="datepicker" class="calendar-datepicker" role="presentation"></div>
</div>
<!-- calendar -->
<div id="layout-content" class="selected no-navbar" role="main">
<h2 id="aria-label-toolbar" class="voice"><roundcube:label name="arialabeltoolbar" /></h2>
<div class="header" role="toolbar" aria-labelledby="aria-label-toolbar">
<a class="button icon task-menu-button" href="#menu"><span class="inner"><roundcube:label name="menu" /></span></a>
<a class="button icon back-sidebar-button folders" href="#sidebar" data-hidden="big"><span class="inner"><roundcube:label name="calendar.calendars" /></span></a>
<span class="header-title"></span>
<!-- toolbar -->
<div id="calendartoolbar" class="toolbar menu">
<roundcube:button command="addevent" type="link"
class="button create disabled" classAct="button create"
label="create" title="calendar.new_event" innerClass="inner" />
<roundcube:button command="print" type="link" data-hidden="small"
class="button print disabled" classAct="button print"
label="calendar.print" title="calendar.printtitle" innerClass="inner" />
<span class="spacer"></span>
<roundcube:button command="events-import" type="link"
class="button import disabled" classAct="button import"
label="import" title="calendar.importevents" innerClass="inner" />
<roundcube:button command="export" type="link"
class="button export disabled" classAct="button export"
label="calendar.export" title="calendar.exporttitle" innerClass="inner" />
<roundcube:container name="toolbar" id="calendartoolbar" />
</div>
</div>
<roundcube:object name="plugin.searchform" id="searchform" wrapper="searchbar menu"
label="searchform" buttontitle="calendar.findevents" label-domain="calendar" ariatag="h2" />
<h2 id="aria-label-calendarview" class="voice"><roundcube:label name="calendar.arialabelcalendarview" /></h2>
<div id="calendar" class="content" role="main" aria-labelledby="aria-label-calendarview" data-elastic-mode="true">
<roundcube:object name="plugin.agenda_options" id="agendaoptions" />
<div id="searchcontrols" class="search-controls"></div>
</div>
<div class="footer toolbar menu content-frame-navigation" role="toolbar" data-hidden="big">
<a href="#" class="button prev" onclick="$('.fc-prev-button').click()"><span class="inner"><roundcube:label name="previous" /></span></a>
<a href="#" class="button today" onclick="$('.fc-today-button').click()"><span class="inner"><roundcube:label name="today" /></span></a>
<a href="#" class="button date" onclick="window.calendar_datepicker()"><span class="inner"><roundcube:label name="date" /></span></a>
<a href="#" class="button next" onclick="$('.fc-next-button').click()"><span class="inner"><roundcube:label name="next" /></span></a>
</div>
</div>
<div id="timezone-display" class="hidden"><roundcube:var name="env:timezone" /></div>
<div id="eventshow" class="popupmenu formcontent propform text-only">
<h1 id="event-title" class="event-title form-group">Event Title</h1>
<div id="event-status-badge"><span></span></div>
<div class="event-location form-group" id="event-location">Location</div>
<div class="event-date form-group" id="event-date">From-To</div>
<div class="event-description form-group" id="event-description">
<div class="event-text"></div>
</div>
<div class="event-attendees form-group" id="event-attendees">
<div class="event-text"></div>
</div>
<div id="event-url" class="form-group row">
<label class="col-sm-4 col-form-label"><roundcube:label name="calendar.url" /></label>
<span class="event-text col-sm-8 form-control-plaintext"></span>
</div>
<div id="event-repeat" class="form-group row">
<label class="col-sm-4 col-form-label"><roundcube:label name="calendar.repeat" /></label>
<span class="event-text col-sm-8 form-control-plaintext"></span>
</div>
<div id="event-alarm" class="form-group row">
<label class="col-sm-4 col-form-label"><roundcube:label name="calendar.alarms" /></label>
<span class="event-text col-sm-8 form-control-plaintext"></span>
</div>
<div id="event-partstat" class="form-group row event-partstat">
<label class="col-sm-4 col-form-label"><roundcube:label name="calendar.mystatus" /></label>
<span class="col-sm-8 form-control-plaintext">
<span class="event-text rsvp-status"></span>
<a class="changersvp button edit" href="#" title="<roundcube:label name='calendar.changepartstat' />"><span class="inner"><roundcube:label name='calendar.changepartstat' /></span></a>
</span>
</div>
<div id="event-calendar" class="form-group row">
<label class="col-sm-4 col-form-label"><roundcube:label name="calendar.calendar" /></label>
<span class="col-sm-8 form-control-plaintext event-text">Default</span>
</div>
<div id="event-category" class="form-group row">
<label class="col-sm-4 col-form-label"><roundcube:label name="calendar.category" /></label>
<span class="col-sm-8 form-control-plaintext event-text"></span>
</div>
<div id="event-status" class="form-group row">
<label class="col-sm-4 col-form-label"><roundcube:label name="calendar.status" /></label>
<span class="event-text col-sm-8 form-control-plaintext"></span>
</div>
<div id="event-free-busy" class="form-group row">
<label class="col-sm-4 col-form-label"><roundcube:label name="calendar.freebusy" /></label>
<span class="event-text col-sm-8 form-control-plaintext"></span>
</div>
<div id="event-priority" class="form-group row">
<label class="col-sm-4 col-form-label"><roundcube:label name="calendar.priority" /></label>
<span class="event-text col-sm-8 form-control-plaintext"></span>
</div>
<div id="event-rsvp-comment" class="form-group row">
<label class="col-sm-4 col-form-label"><roundcube:label name="calendar.rsvpcomment" /></label>
<span class="event-text col-sm-8 form-control-plaintext"></span>
</div>
<div id="event-links" class="form-group row">
<label class="col-sm-4 col-form-label"><roundcube:label name="calendar.links" /></label>
<span class="event-text col-sm-8"></span>
</div>
<div id="event-attachments" class="form-group row">
<label class="col-sm-4 col-form-label"><roundcube:label name="attachments" /></label>
<span class="event-text col-sm-8"></span>
</div>
<div id="event-created" class="form-group row faded">
<label class="col-sm-4 col-form-label"><roundcube:label name="calendar.created" /></label>
<span class="event-text event-created col-sm-8 form-control-plaintext"></span>
</div>
<div id="event-changed" class="form-group row faded">
<label class="col-sm-4 col-form-label"><roundcube:label name="calendar.changed" /></label>
<span class="event-text event-changed col-sm-8 form-control-plaintext"></span>
</div>
<roundcube:object name="plugin.event_rsvp_buttons" id="event-rsvp" class="calendar-invitebox invitebox boxinformation" style="display:none" />
</div>
<roundcube:include file="/templates/eventedit.html" />
<div id="calendaractions-menu" class="popupmenu">
<h3 id="aria-label-calendaroptions" class="voice"><roundcube:label name="calendar.calendaractions" /></h3>
<ul class="menu listing" role="menu" aria-labelledby="aria-label-calendaroptions">
+ <roundcube:if condition="env:calendar_driver != 'caldav'" />
<roundcube:button type="link-menuitem" command="calendar-create" label="calendar.addcalendar" class="create disabled" classAct="create active" />
<roundcube:button type="link-menuitem" command="calendar-edit" label="calendar.editcalendar" class="edit disabled" classAct="edit active" />
<roundcube:button type="link-menuitem" command="calendar-delete" label="calendar.deletecalendar" class="delete disabled" classAct="delete active" />
+ <roundcube:endif />
<roundcube:if condition="env:calendar_driver == 'kolab'" />
<roundcube:button type="link-menuitem" command="calendar-remove" label="calendar.removelist" class="remove disabled" classAct="remove active" />
<roundcube:endif />
<roundcube:button type="link-menuitem" command="calendar-showurl" label="calendar.showurl" class="showurl disabled" classAct="showurl active" />
<roundcube:if condition="!empty(env:calendar_settings['freebusy_url'])" />
<roundcube:button type="link-menuitem" command="calendar-showfburl" label="calendar.showfburl" class="showurl disabled" classAct="showurl active" />
<roundcube:endif />
<roundcube:if condition="env:calendar_driver == 'kolab'" />
<roundcube:button type="link-menuitem" command="folders" task="settings" label="managefolders" class="folders disabled" classAct="folders active" />
<roundcube:endif />
</ul>
</div>
<div id="eventoptionsmenu" class="popupmenu">
<h3 id="aria-label-eventoptions" class="voice"><roundcube:label name="calendar.eventoptions" /></h3>
<ul class="menu listing" role="menu" aria-labelledby="aria-label-eventoptions">
<roundcube:button type="link-menuitem" command="event-download" label="download" class="download disabled" classAct="download active" />
<roundcube:button type="link-menuitem" command="event-sendbymail" label="send" class="send disabled" classAct="send active" />
<roundcube:button type="link-menuitem" command="event-copy" label="copy" class="copy disabled" classAct="copy active" />
<roundcube:if condition="env:calendar_driver == 'kolab' && config:kolab_bonnie_api" />
<roundcube:button type="link-menuitem" command="event-history" label="calendar.eventhistory" class="history disabled" classAct="history active" />
<roundcube:endif />
</ul>
</div>
<div id="eventresourcesdialog" class="popupmenu">
<h3 class="voice" id="aria-label-resourceselection"><roundcube:label name="calendar.arialabelresourceselection" /></h3>
<div class="resource-selection selection-list" role="navigation" aria-labelledby="aria-label-resourceselection">
<div class="header">
<span class="header-title"><roundcube:label name="calendar.tabresources" /></span>
</div>
<roundcube:object name="plugin.resources_searchform" id="resourcesearchbox"
wrapper="searchbar menu" ariatag="h4" buttontitle="calendar.findresources"
label="resourcesearchform" label-domain="calendar" />
<div class="scroller">
<roundcube:object name="plugin.resources_list" id="resources-list" class="listing treelist" />
</div>
</div>
<div class="resource-content selection-content">
<div class="header" data-hidden="normal,big">
<a class="button icon back" href="#back" onclick="$('#eventresourcesdialog .resource-content').hide(); $('#eventresourcesdialog .resource-selection').show()">
<span class="inner"><roundcube:label name="back" /></span>
</a>
<span class="header-title"><roundcube:label name="calendar.resourceprops" /></span>
</div>
<ul class="nav nav-tabs" role="tablist">
<li class="nav-item">
<a class="nav-link active" href="#resource-availability" data-toggle="tab" role="tab">
<roundcube:label name="calendar.resourceavailability" />
</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#resource-info" data-toggle="tab" role="tab">
<roundcube:label name="calendar.resourcedetails" />
</a>
</li>
</ul>
<div class="tab-content">
<div id="resource-availability" class="tab-pane active" role="tabpanel" aria-labelledby="aria-label-resourceavailability">
<h3 class="voice" id="aria-label-resourceavailability"><roundcube:label name="calendar.resourceavailability" /></h3>
<roundcube:object name="plugin.resource_calendar" id="resource-freebusy-calendar" class="raw-tables" />
<div class="slot-nav">
<roundcube:button name="resource-cal-prev" id="resource-calendar-prev" type="link"
class="icon prevpage" title="calendar.prevslot" label="calendar.prevweek" />
<roundcube:button name="resource-cal-next" id="resource-calendar-next" type="link"
class="icon nextpage" title="calendar.nextslot" label="calendar.nextweek" />
</div>
</div>
<div id="resource-info" class="tab-pane" role="tabpanel" aria-labelledby="aria-label-resourcedetails">
<h3 class="voice" id="aria-label-resourcedetails"><roundcube:label name="calendar.resourcedetails" /></h3>
<roundcube:object name="plugin.resource_info" id="resource-details" class="propform text-only"
aria-live="polite" aria-relevant="text" aria-atomic="true" />
</div>
</div>
</div>
</div>
<div id="eventfreebusy" class="popupmenu calendar-scheduler formcontent">
<roundcube:object name="plugin.attendees_freebusy_table" id="attendees-freebusy-table"
class="schedule-table" data-h-margin="-1" data-v-margin="1" />
<div class="nav">
<div class="schedule-buttons">
<button type="button" id="schedule-find-prev" class="btn btn-secondary prev-slot"><roundcube:label name="calendar.prevslot" /></button>
<button type="button" id="schedule-find-next" class="btn btn-secondary next-slot"><roundcube:label name="calendar.nextslot" /></button>
</div>
<div class="schedule-options">
<label><input type="checkbox" id="schedule-freebusy-workinghours" value="1" class="pretty-checkbox" /><roundcube:label name="calendar.onlyworkinghours" /></label>
</div>
<div class="schedule-nav">
<button type="button" id="schedule-freebusy-prev" title="<roundcube:label name='previouspage' />">◄</button>
<button type="button" id="schedule-freebusy-next" title="<roundcube:label name='nextpage' />">►</button>
</div>
</div>
<div class="schedule-range">
<div class="form-group row">
<label for="schedule-startdate" class="col-form-label col-sm-2"><roundcube:label name="calendar.start" /></label>
<span class="col-sm-10 datetime">
<input type="text" name="startdate" size="11" id="schedule-startdate" class="form-control" disabled="true" />
<input type="text" name="starttime" size="6" id="schedule-starttime" class="form-control" disabled="true" />
</span>
</div>
<div class="form-group row">
<label for="schedule-enddate" class="col-form-label col-sm-2"><roundcube:label name="calendar.end" /></label>
<span class="col-sm-10 datetime">
<input type="text" name="enddate" size="11" id="schedule-enddate" class="form-control" disabled="true" />
<input type="text" name="endtime" size="6" id="schedule-endtime" class="form-control" disabled="true" />
</span>
</div>
<div class="schedule-legend form-group row">
<label class="col-form-label col-sm-2"><roundcube:label name="calendar.legend" /></label>
<span class="col-sm-10 form-control-plaintext">
<roundcube:include file="/templates/freebusylegend.html" />
<span class="attendee organizer"><roundcube:label name="calendar.roleorganizer" /></span>
<span class="attendee req-participant"><roundcube:label name="calendar.rolerequired" /></span>
<span class="attendee opt-participant"><roundcube:label name="calendar.roleoptional" /></span>
<span class="attendee non-participant"><roundcube:label name="calendar.rolenonparticipant" /></span>
<span class="attendee chair"><roundcube:label name="calendar.rolechair" /></span>
</span>
</div>
</div>
</div>
<div id="eventsimport" class="popupmenu formcontent">
<roundcube:object name="plugin.events_import_form" id="events-import-form" />
</div>
<div id="eventsexport" class="popupmenu formcontent">
<roundcube:object name="plugin.events_export_form" id="events-export-form" />
</div>
<div id="calendarurlbox" class="popupmenu">
<p><roundcube:label name="calendar.showurldescription" /></p>
<textarea id="calfeedurl" rows="2" readonly="readonly"></textarea>
<div id="calendarcaldavurl" style="display:none; margin-top:1rem">
<p><roundcube:label name="calendar.caldavurldescription" html="yes" /></p>
<textarea id="caldavurl" rows="2" readonly="readonly"></textarea>
</div>
</div>
<div id="fburlbox" class="popupmenu">
<p><roundcube:label name="calendar.fburldescription" /></p>
<textarea id="fburl" rows="2" readonly="readonly"></textarea>
</div>
<roundcube:if condition="config:kolab_bonnie_api" />
<div id="eventhistory" class="popupmenu" aria-hidden="true">
<roundcube:object name="plugin.object_changelog_table" id="event-changelog-table" class="changelog-table" />
<div class="compare-button"><input type="button" class="button" value="<roundcube:label name='libkolab.compare' />" /></div>
</div>
<div id="eventdiff" class="popupmenu formcontent text-only">
<h1 class="event-title">Event Title</h1>
<h1 class="event-title-new event-text-new"></h1>
<div class="form-group row event-date"></div>
<div class="form-group row event-location">
<h5 class="label"><roundcube:label name="calendar.location" /></h5>
<div class="event-text-old"></div>
<div class="event-text-new"></div>
</div>
<div class="form-group row event-description">
<h5 class="label"><roundcube:label name="calendar.description" /></h5>
<div class="event-text-diff" style="white-space:pre-wrap"></div>
<div class="event-text-old"></div>
<div class="event-text-new"></div>
</div>
<div class="form-group row event-url">
<h5 class="label"><roundcube:label name="calendar.url" /></h5>
<div class="event-text-old"></div>
<div class="event-text-new"></div>
</div>
<div class="form-group row event-recurrence">
<h5 class="label"><roundcube:label name="calendar.repeat" /></h5>
<div class="event-text-old"></div>
<div class="event-text-new"></div>
</div>
<div class="form-group row event-alarms">
<h5 class="label"><roundcube:label name="calendar.alarms" /><span class="index"></span></h5>
<div class="event-text-old"></div>
<div class="event-text-new"></div>
</div>
<div class="event-line event-start">
<label><roundcube:label name="calendar.start" /></label>
<span class="event-text-old"></span> ⇢
<span class="event-text-new"></span>
</div>
<div class="event-line event-end">
<label><roundcube:label name="calendar.end" /></label>
<span class="event-text-old"></span> ⇢
<span class="event-text-new"></span>
</div>
<div class="event-line event-attendees">
<label><roundcube:label name="calendar.tabattendees" /><span class="index"></span></label>
<span class="event-text-old"></span> ⇢
<span class="event-text-new"></span>
</div>
<div class="event-line event-calendar">
<label><roundcube:label name="calendar.calendar" /></label>
<span class="event-text-old"></span> ⇢
<span class="event-text-new"></span>
</div>
<div class="event-line event-categories">
<label><roundcube:label name="calendar.category" /></label>
<span class="event-text-old"></span> ⇢
<span class="event-text-new"></span>
</div>
<div class="event-line event-status">
<label><roundcube:label name="calendar.status" /></label>
<span class="event-text-old"></span> ⇢
<span class="event-text-new"></span>
</div>
<div class="event-line event-free_busy">
<label><roundcube:label name="calendar.freebusy" /></label>
<span class="event-text-old"></span> ⇢
<span class="event-text-new"></span>
</div>
<div class="event-line event-priority">
<label><roundcube:label name="calendar.priority" /></label>
<span class="event-text-old"></span> ⇢
<span class="event-text-new"></span>
</div>
<div class="form-group row event-attachments">
<label><roundcube:label name="attachments" /><span class="index"></span></label>
<div class="event-text-old"></div>
<div class="event-text-new"></div>
</div>
</div>
<roundcube:endif />
<roundcube:object name="plugin.calendar_css" folder-class="div.$class a.calname:before" folder-fallback-color="#161b1d" />
<roundcube:include file="includes/footer.html" />
diff --git a/plugins/kolab_addressbook/kolab_addressbook.php b/plugins/kolab_addressbook/kolab_addressbook.php
index 923b75a5..efbf74c8 100644
--- a/plugins/kolab_addressbook/kolab_addressbook.php
+++ b/plugins/kolab_addressbook/kolab_addressbook.php
@@ -1,1199 +1,1200 @@
<?php
/**
* Kolab address book
*
* Sample plugin to add a new address book source with data from Kolab storage
* It provides also a possibilities to manage contact folders
* (create/rename/delete/acl) directly in Addressbook UI.
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
* @author Aleksander Machniak <machniak@kolabsys.com>
*
* Copyright (C) 2011-2015, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_addressbook extends rcube_plugin
{
public $task = '?(?!logout).*';
private $sources;
private $folders;
private $rc;
private $ui;
public $bonnie_api = false;
const GLOBAL_FIRST = 0;
const PERSONAL_FIRST = 1;
const GLOBAL_ONLY = 2;
const PERSONAL_ONLY = 3;
/**
* Startup method of a Roundcube plugin
*/
public function init()
{
- require_once(dirname(__FILE__) . '/lib/rcube_kolab_contacts.php');
-
$this->rc = rcube::get_instance();
// load required plugin
$this->require_plugin('libkolab');
+ $driver = $this->rc->config->get('kolab_addressbook_driver') ?: 'kolab';
+ require_once(dirname(__FILE__) . '/lib/rcube_' . $driver . '_contacts.php');
+
// 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');
$this->add_hook('contact_form', array($this, 'contact_form'));
$this->add_hook('contact_photo', array($this, 'contact_photo'));
$this->add_hook('template_object_directorylist', array($this, 'directorylist_html'));
// Plugin actions
$this->register_action('plugin.book', array($this, 'book_actions'));
$this->register_action('plugin.book-save', array($this, 'book_save'));
$this->register_action('plugin.book-search', array($this, 'book_search'));
$this->register_action('plugin.book-subscribe', array($this, 'book_subscribe'));
$this->register_action('plugin.contact-changelog', array($this, 'contact_changelog'));
$this->register_action('plugin.contact-diff', array($this, 'contact_diff'));
$this->register_action('plugin.contact-restore', array($this, 'contact_restore'));
// get configuration for the Bonnie API
$this->bonnie_api = libkolab::get_bonnie_api();
// Load UI elements
if ($this->api->output->type == 'html') {
$this->load_config();
require_once($this->home . '/lib/kolab_addressbook_ui.php');
$this->ui = new kolab_addressbook_ui($this);
if ($this->bonnie_api) {
$this->add_button(array(
'command' => 'contact-history-dialog',
'class' => 'history contact-history disabled',
'classact' => 'history contact-history active',
'innerclass' => 'icon inner',
'label' => 'kolab_addressbook.showhistory',
'type' => 'link-menuitem'
), 'contactmenu');
}
}
}
else if ($this->rc->task == 'settings') {
$this->add_texts('localization');
$this->add_hook('preferences_list', array($this, 'prefs_list'));
$this->add_hook('preferences_save', array($this, 'prefs_save'));
}
$this->add_hook('folder_delete', array($this, 'prefs_folder_delete'));
$this->add_hook('folder_rename', array($this, 'prefs_folder_rename'));
$this->add_hook('folder_update', array($this, 'prefs_folder_update'));
}
/**
* Handler for the addressbooks_list hook.
*
* This will add all instances of available Kolab-based address books
* to the list of address sources of Roundcube.
* This will also hide some addressbooks according to kolab_addressbook_prio setting.
*
* @param array $p Hash array with hook parameters
*
* @return array Hash array with modified hook parameters
*/
public function address_sources($p)
{
$abook_prio = $this->addressbook_prio();
// Disable all global address books
// Assumes that all non-kolab_addressbook sources are global
if ($abook_prio == self::PERSONAL_ONLY) {
$p['sources'] = array();
}
$sources = array();
foreach ($this->_list_sources() as $abook_id => $abook) {
// register this address source
$sources[$abook_id] = $this->abook_prop($abook_id, $abook);
// flag folders with 'i' right as writeable
if ($this->rc->action == 'add' && strpos($abook->rights, 'i') !== false) {
$sources[$abook_id]['readonly'] = false;
}
}
// Add personal address sources to the list
if ($abook_prio == self::PERSONAL_FIRST) {
// $p['sources'] = array_merge($sources, $p['sources']);
// Don't use array_merge(), because if you have folders name
// that resolve to numeric identifier it will break output array keys
foreach ($p['sources'] as $idx => $value)
$sources[$idx] = $value;
$p['sources'] = $sources;
}
else {
// $p['sources'] = array_merge($p['sources'], $sources);
foreach ($sources as $idx => $value)
$p['sources'][$idx] = $value;
}
return $p;
}
/**
* Helper method to build a hash array of address book properties
*/
protected function abook_prop($id, $abook)
{
if ($abook->virtual) {
return array(
'id' => $id,
'name' => $abook->get_name(),
'listname' => $abook->get_foldername(),
'group' => $abook instanceof kolab_storage_folder_user ? 'user' : $abook->get_namespace(),
'readonly' => true,
'rights' => 'l',
'kolab' => true,
'virtual' => true,
);
}
else {
return array(
'id' => $id,
'name' => $abook->get_name(),
'listname' => $abook->get_foldername(),
'readonly' => $abook->readonly,
'rights' => $abook->rights,
'groups' => $abook->groups,
'undelete' => $abook->undelete && $this->rc->config->get('undo_timeout'),
'realname' => rcube_charset::convert($abook->get_realname(), 'UTF7-IMAP'), // IMAP folder name
'group' => $abook->get_namespace(),
'subscribed' => $abook->is_subscribed(),
'carddavurl' => $abook->get_carddav_url(),
'removable' => true,
'kolab' => true,
'audittrail' => !empty($this->bonnie_api),
);
}
}
/**
*
*/
public function directorylist_html($args)
{
$out = '';
$jsdata = array();
$sources = (array)$this->rc->get_address_sources();
// list all non-kolab sources first (also exclude hidden sources)
$filter = function($source){ return empty($source['kolab']) && empty($source['hidden']); };
foreach (array_filter($sources, $filter) as $j => $source) {
$id = strval(strlen($source['id']) ? $source['id'] : $j);
$out .= $this->addressbook_list_item($id, $source, $jsdata) . '</li>';
}
// render a hierarchical list of kolab contact folders
kolab_storage::folder_hierarchy($this->folders, $tree);
if ($tree && !empty($tree->children)) {
$out .= $this->folder_tree_html($tree, $sources, $jsdata);
}
$this->rc->output->set_env('contactgroups', array_filter($jsdata, function($src){ return $src['type'] == 'group'; }));
$this->rc->output->set_env('address_sources', array_filter($jsdata, function($src){ return $src['type'] != 'group'; }));
$args['content'] = html::tag('ul', $args, $out, html::$common_attrib);
return $args;
}
/**
* Return html for a structured list <ul> for the folder tree
*/
public function folder_tree_html($node, $data, &$jsdata)
{
$out = '';
foreach ($node->children as $folder) {
$id = $folder->id;
$source = $data[$id];
$is_collapsed = strpos($this->rc->config->get('collapsed_abooks',''), '&'.rawurlencode($id).'&') !== false;
if ($folder->virtual) {
$source = $this->abook_prop($folder->id, $folder);
}
else if (empty($source)) {
$this->sources[$id] = new rcube_kolab_contacts($folder->name);
$source = $this->abook_prop($id, $this->sources[$id]);
}
$content = $this->addressbook_list_item($id, $source, $jsdata);
if (!empty($folder->children)) {
$child_html = $this->folder_tree_html($folder, $data, $jsdata);
// copy group items...
if (preg_match('!<ul[^>]*>(.*)</ul>\n*$!Ums', $content, $m)) {
$child_html = $m[1] . $child_html;
$content = substr($content, 0, -strlen($m[0]) - 1);
}
// ... and re-create the subtree
if (!empty($child_html)) {
$content .= html::tag('ul', array('class' => 'groups', 'style' => ($is_collapsed ? "display:none;" : null)), $child_html);
}
}
$out .= $content . '</li>';
}
return $out;
}
/**
*
*/
protected function addressbook_list_item($id, $source, &$jsdata, $search_mode = false)
{
$current = rcube_utils::get_input_value('_source', rcube_utils::INPUT_GPC);
if (!$source['virtual']) {
$jsdata[$id] = $source;
$jsdata[$id]['name'] = html_entity_decode($source['name'], ENT_NOQUOTES, RCUBE_CHARSET);
}
// set class name(s)
$classes = array('addressbook');
if ($source['group'])
$classes[] = $source['group'];
if ($current === $id)
$classes[] = 'selected';
if ($source['readonly'])
$classes[] = 'readonly';
if ($source['virtual'])
$classes[] = 'virtual';
if ($source['class_name'])
$classes[] = $source['class_name'];
$name = !empty($source['listname']) ? $source['listname'] : (!empty($source['name']) ? $source['name'] : $id);
$label_id = 'kabt:' . $id;
$inner = ($source['virtual'] ?
html::a(array('tabindex' => '0'), $name) :
html::a(array(
'href' => $this->rc->url(array('_source' => $id)),
'rel' => $source['id'],
'id' => $label_id,
'onclick' => "return " . rcmail_output::JS_OBJECT_NAME.".command('list','" . rcube::JQ($id) . "',this)",
), $name)
);
if (isset($source['subscribed'])) {
$inner .= html::span(array(
'class' => 'subscribed',
'title' => $this->gettext('foldersubscribe'),
'role' => 'checkbox',
'aria-checked' => $source['subscribed'] ? 'true' : 'false',
), '');
}
// don't wrap in <li> but add a checkbox for search results listing
if ($search_mode) {
$jsdata[$id]['group'] = join(' ', $classes);
if (!$source['virtual']) {
$inner .= html::tag('input', array(
'type' => 'checkbox',
'name' => '_source[]',
'value' => $id,
'checked' => false,
'aria-labelledby' => $label_id,
));
}
return html::div(null, $inner);
}
$out .= html::tag('li', array(
'id' => 'rcmli' . rcube_utils::html_identifier($id, true),
'class' => join(' ', $classes),
'noclose' => true,
),
html::div($source['subscribed'] ? 'subscribed' : null, $inner)
);
$groupdata = array('out' => '', 'jsdata' => $jsdata, 'source' => $id);
if ($source['groups'] && function_exists('rcmail_contact_groups')) {
$groupdata = rcmail_contact_groups($groupdata);
}
$jsdata = $groupdata['jsdata'];
$out .= $groupdata['out'];
return $out;
}
/**
* Sets autocomplete_addressbooks option according to
* kolab_addressbook_prio setting extending list of address sources
* to be used for autocompletion.
*/
public function config_get($args)
{
if ($args['name'] != 'autocomplete_addressbooks' || $this->recurrent) {
return $args;
}
$abook_prio = $this->addressbook_prio();
// Get the original setting, use temp flag to prevent from an infinite recursion
$this->recurrent = true;
$sources = $this->rc->config->get('autocomplete_addressbooks');
$this->recurrent = false;
// Disable all global address books
// Assumes that all non-kolab_addressbook sources are global
if ($abook_prio == self::PERSONAL_ONLY) {
$sources = array();
}
if (!is_array($sources)) {
$sources = array();
}
$kolab_sources = array();
foreach (array_keys($this->_list_sources()) as $abook_id) {
if (!in_array($abook_id, $sources))
$kolab_sources[] = $abook_id;
}
// Add personal address sources to the list
if (!empty($kolab_sources)) {
if ($abook_prio == self::PERSONAL_FIRST) {
$sources = array_merge($kolab_sources, $sources);
}
else {
$sources = array_merge($sources, $kolab_sources);
}
}
$args['result'] = $sources;
return $args;
}
/**
* Getter for the rcube_addressbook instance
*
* @param array $p Hash array with hook parameters
*
* @return array Hash array with modified hook parameters
*/
public function get_address_book($p)
{
if ($p['id']) {
$id = kolab_storage::id_decode($p['id']);
$folder = kolab_storage::get_folder($id);
// try with unencoded (old-style) identifier
if ((!$folder || $folder->type != 'contact') && $id != $p['id']) {
$folder = kolab_storage::get_folder($p['id']);
}
if ($folder && $folder->type == 'contact') {
$p['instance'] = new rcube_kolab_contacts($folder->name);
// flag source as writeable if 'i' right is given
if ($p['writeable'] && $this->rc->action == 'save' && strpos($p['instance']->rights, 'i') !== false) {
$p['instance']->readonly = false;
}
else if ($this->rc->action == 'delete' && strpos($p['instance']->rights, 't') !== false) {
$p['instance']->readonly = false;
}
}
}
return $p;
}
private function _list_sources()
{
// already read sources
if (isset($this->sources))
return $this->sources;
kolab_storage::$encode_ids = true;
$this->sources = array();
$this->folders = array();
$abook_prio = $this->addressbook_prio();
// Personal address source(s) disabled?
if ($abook_prio == self::GLOBAL_ONLY) {
return $this->sources;
}
// get all folders that have "contact" type
$folders = kolab_storage::sort_folders(kolab_storage::get_folders('contact'));
if (PEAR::isError($folders)) {
rcube::raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Failed to list contact folders from Kolab server:" . $folders->getMessage()),
true, false);
}
else {
// we need at least one folder to prevent from errors in Roundcube core
// when there's also no sql nor ldap addressbook (Bug #2086)
if (empty($folders)) {
if ($folder = kolab_storage::create_default_folder('contact')) {
$folders = array(new kolab_storage_folder($folder, 'contact'));
}
}
// convert to UTF8 and sort
foreach ($folders as $folder) {
// create instance of rcube_contacts
$abook_id = $folder->id;
$abook = new rcube_kolab_contacts($folder->name);
$this->sources[$abook_id] = $abook;
$this->folders[$abook_id] = $folder;
}
}
return $this->sources;
}
/**
* Plugin hook called before rendering the contact form or detail view
*
* @param array $p Hash array with hook parameters
*
* @return array Hash array with modified hook parameters
*/
public function contact_form($p)
{
// none of our business
if (!is_object($GLOBALS['CONTACTS']) || !is_a($GLOBALS['CONTACTS'], 'rcube_kolab_contacts'))
return $p;
// extend the list of contact fields to be displayed in the 'personal' section
if (is_array($p['form']['personal'])) {
$p['form']['personal']['content']['profession'] = array('size' => 40);
$p['form']['personal']['content']['children'] = array('size' => 40);
$p['form']['personal']['content']['freebusyurl'] = array('size' => 40);
$p['form']['personal']['content']['pgppublickey'] = array('size' => 70);
$p['form']['personal']['content']['pkcs7publickey'] = array('size' => 70);
// re-order fields according to the coltypes list
$p['form']['contact']['content'] = $this->_sort_form_fields($p['form']['contact']['content'], $GLOBALS['CONTACTS']);
$p['form']['personal']['content'] = $this->_sort_form_fields($p['form']['personal']['content'], $GLOBALS['CONTACTS']);
/* define a separate section 'settings'
$p['form']['settings'] = array(
'name' => $this->gettext('settings'),
'content' => array(
'freebusyurl' => array('size' => 40, 'visible' => true),
'pgppublickey' => array('size' => 70, 'visible' => true),
'pkcs7publickey' => array('size' => 70, 'visible' => false),
)
);
*/
}
if ($this->bonnie_api && $this->rc->action == 'show' && empty($p['record']['rev'])) {
$this->rc->output->set_env('kolab_audit_trail', true);
}
return $p;
}
/**
* Plugin hook for the contact photo image
*/
public function contact_photo($p)
{
// add photo data from old revision inline as data url
if (!empty($p['record']['rev']) && !empty($p['data'])) {
$p['url'] = 'data:image/gif;base64,' . base64_encode($p['data']);
}
return $p;
}
/**
* Handler for contact audit trail changelog requests
*/
public function contact_changelog()
{
if (empty($this->bonnie_api)) {
return false;
}
$contact = rcube_utils::get_input_value('cid', rcube_utils::INPUT_POST, true);
$source = rcube_utils::get_input_value('source', rcube_utils::INPUT_POST);
list($uid, $mailbox, $msguid) = $this->_resolve_contact_identity($contact, $source);
$result = $uid && $mailbox ? $this->bonnie_api->changelog('contact', $uid, $mailbox, $msguid) : null;
if (is_array($result) && $result['uid'] == $uid) {
if (is_array($result['changes'])) {
$rcmail = $this->rc;
$dtformat = $this->rc->config->get('date_format') . ' ' . $this->rc->config->get('time_format');
array_walk($result['changes'], function(&$change) use ($rcmail, $dtformat) {
if ($change['date']) {
$dt = rcube_utils::anytodatetime($change['date']);
if ($dt instanceof DateTime) {
$change['date'] = $rcmail->format_date($dt, $dtformat);
}
}
});
}
$this->rc->output->command('contact_render_changelog', $result['changes']);
}
else {
$this->rc->output->command('contact_render_changelog', false);
}
$this->rc->output->send();
}
/**
* Handler for audit trail diff view requests
*/
public function contact_diff()
{
if (empty($this->bonnie_api)) {
return false;
}
$contact = rcube_utils::get_input_value('cid', rcube_utils::INPUT_POST, true);
$source = rcube_utils::get_input_value('source', rcube_utils::INPUT_POST);
$rev1 = rcube_utils::get_input_value('rev1', rcube_utils::INPUT_POST);
$rev2 = rcube_utils::get_input_value('rev2', rcube_utils::INPUT_POST);
list($uid, $mailbox, $msguid) = $this->_resolve_contact_identity($contact, $source);
$result = $this->bonnie_api->diff('contact', $uid, $rev1, $rev2, $mailbox, $msguid);
if (is_array($result) && $result['uid'] == $uid) {
$result['rev1'] = $rev1;
$result['rev2'] = $rev2;
$result['cid'] = $contact;
// convert some properties, similar to rcube_kolab_contacts::_to_rcube_contact()
$keymap = array(
'lastmodified-date' => 'changed',
'additional' => 'middlename',
'fn' => 'name',
'tel' => 'phone',
'url' => 'website',
'bday' => 'birthday',
'note' => 'notes',
'role' => 'profession',
'title' => 'jobtitle',
);
$propmap = array('email' => 'address', 'website' => 'url', 'phone' => 'number');
$date_format = $this->rc->config->get('date_format', 'Y-m-d');
// map kolab object properties to keys and values the client expects
array_walk($result['changes'], function(&$change, $i) use ($keymap, $propmap, $date_format) {
if (array_key_exists($change['property'], $keymap)) {
$change['property'] = $keymap[$change['property']];
}
// format date-time values
if ($change['property'] == 'created' || $change['property'] == 'changed') {
if ($old_ = rcube_utils::anytodatetime($change['old'])) {
$change['old_'] = $this->rc->format_date($old_);
}
if ($new_ = rcube_utils::anytodatetime($change['new'])) {
$change['new_'] = $this->rc->format_date($new_);
}
}
// format dates
else if ($change['property'] == 'birthday' || $change['property'] == 'anniversary') {
if ($old_ = rcube_utils::anytodatetime($change['old'])) {
$change['old_'] = $this->rc->format_date($old_, $date_format);
}
if ($new_ = rcube_utils::anytodatetime($change['new'])) {
$change['new_'] = $this->rc->format_date($new_, $date_format);
}
}
// convert email, website, phone values
else if (array_key_exists($change['property'], $propmap)) {
$propname = $propmap[$change['property']];
foreach (array('old','new') as $k) {
$k_ = $k . '_';
if (!empty($change[$k])) {
$change[$k_] = html::quote($change[$k][$propname] ?: '--');
if ($change[$k]['type']) {
$change[$k_] .= ' ' . html::span('subtype', rcmail_get_type_label($change[$k]['type']));
}
$change['ishtml'] = true;
}
}
}
// serialize address structs
if ($change['property'] == 'address') {
foreach (array('old','new') as $k) {
$k_ = $k . '_';
$change[$k]['zipcode'] = $change[$k]['code'];
$template = $this->rc->config->get('address_template', '{'.join('} {', array_keys($change[$k])).'}');
$composite = array();
foreach ($change[$k] as $p => $val) {
if (strlen($val))
$composite['{'.$p.'}'] = $val;
}
$change[$k_] = preg_replace('/\{\w+\}/', '', strtr($template, $composite));
if ($change[$k]['type']) {
$change[$k_] .= html::div('subtype', rcmail_get_type_label($change[$k]['type']));
}
$change['ishtml'] = true;
}
$change['diff_'] = libkolab::html_diff($change['old_'], $change['new_'], true);
}
// localize gender values
else if ($change['property'] == 'gender') {
if ($change['old']) $change['old_'] = $this->rc->gettext($change['old']);
if ($change['new']) $change['new_'] = $this->rc->gettext($change['new']);
}
// translate 'key' entries in individual properties
else if ($change['property'] == 'key') {
$p = $change['old'] ?: $change['new'];
$t = $p['type'];
$change['property'] = $t . 'publickey';
$change['old'] = $change['old'] ? $change['old']['key'] : '';
$change['new'] = $change['new'] ? $change['new']['key'] : '';
}
// compute a nice diff of notes
else if ($change['property'] == 'notes') {
$change['diff_'] = libkolab::html_diff($change['old'], $change['new'], false);
}
});
$this->rc->output->command('contact_show_diff', $result);
}
else {
$this->rc->output->command('display_message', $this->gettext('objectdiffnotavailable'), 'error');
}
$this->rc->output->send();
}
/**
* Handler for audit trail revision restore requests
*/
public function contact_restore()
{
if (empty($this->bonnie_api)) {
return false;
}
$success = false;
$contact = rcube_utils::get_input_value('cid', rcube_utils::INPUT_POST, true);
$source = rcube_utils::get_input_value('source', rcube_utils::INPUT_POST);
$rev = rcube_utils::get_input_value('rev', rcube_utils::INPUT_POST);
list($uid, $mailbox, $msguid) = $this->_resolve_contact_identity($contact, $source, $folder);
if ($folder && ($raw_msg = $this->bonnie_api->rawdata('contact', $uid, $rev, $mailbox))) {
$imap = $this->rc->get_storage();
// insert $raw_msg as new message
if ($imap->save_message($folder->name, $raw_msg, null, false)) {
$success = true;
// delete old revision from imap and cache
$imap->delete_message($msguid, $folder->name);
$folder->cache->set($msguid, false);
$this->cache = array();
}
}
if ($success) {
$this->rc->output->command('display_message', $this->gettext(array('name' => 'objectrestoresuccess', 'vars' => array('rev' => $rev))), 'confirmation');
$this->rc->output->command('close_contact_history_dialog', $contact);
}
else {
$this->rc->output->command('display_message', $this->gettext('objectrestoreerror'), 'error');
}
$this->rc->output->send();
}
/**
* Get a previous revision of the given contact record from the Bonnie API
*/
public function get_revision($cid, $source, $rev)
{
if (empty($this->bonnie_api)) {
return false;
}
list($uid, $mailbox, $msguid) = $this->_resolve_contact_identity($cid, $source);
// call Bonnie API
$result = $this->bonnie_api->get('contact', $uid, $rev, $mailbox, $msguid);
if (is_array($result) && $result['uid'] == $uid && !empty($result['xml'])) {
$format = kolab_format::factory('contact');
$format->load($result['xml']);
$rec = $format->to_array();
if ($format->is_valid()) {
$rec['rev'] = $result['rev'];
return $rec;
}
}
return false;
}
/**
* Helper method to resolved the given contact identifier into uid and mailbox
*
* @return array (uid,mailbox,msguid) tuple
*/
private function _resolve_contact_identity($id, $abook, &$folder = null)
{
$mailbox = $msguid = null;
$source = $this->get_address_book(array('id' => $abook));
if ($source['instance']) {
$uid = $source['instance']->id2uid($id);
$list = kolab_storage::id_decode($abook);
}
else {
return array(null, $mailbox, $msguid);
}
// get resolve message UID and mailbox identifier
if ($folder = kolab_storage::get_folder($list)) {
$mailbox = $folder->get_mailbox_id();
$msguid = $folder->cache->uid2msguid($uid);
}
return array($uid, $mailbox, $msguid);
}
/**
*
*/
private function _sort_form_fields($contents, $source)
{
$block = array();
foreach (array_keys($source->coltypes) as $col) {
if (isset($contents[$col]))
$block[$col] = $contents[$col];
}
return $block;
}
/**
* Handler for user preferences form (preferences_list hook)
*
* @param array $args Hash array with hook parameters
*
* @return array Hash array with modified hook parameters
*/
public function prefs_list($args)
{
if ($args['section'] != 'addressbook') {
return $args;
}
$ldap_public = $this->rc->config->get('ldap_public');
// Hide option if there's no global addressbook
if (empty($ldap_public)) {
return $args;
}
// Check that configuration is not disabled
$dont_override = (array) $this->rc->config->get('dont_override', array());
$prio = $this->addressbook_prio();
if (!in_array('kolab_addressbook_prio', $dont_override)) {
// Load localization
$this->add_texts('localization');
$field_id = '_kolab_addressbook_prio';
$select = new html_select(array('name' => $field_id, 'id' => $field_id));
$select->add($this->gettext('globalfirst'), self::GLOBAL_FIRST);
$select->add($this->gettext('personalfirst'), self::PERSONAL_FIRST);
$select->add($this->gettext('globalonly'), self::GLOBAL_ONLY);
$select->add($this->gettext('personalonly'), self::PERSONAL_ONLY);
$args['blocks']['main']['options']['kolab_addressbook_prio'] = array(
'title' => html::label($field_id, rcube::Q($this->gettext('addressbookprio'))),
'content' => $select->show($prio),
);
}
return $args;
}
/**
* Handler for user preferences save (preferences_save hook)
*
* @param array $args Hash array with hook parameters
*
* @return array Hash array with modified hook parameters
*/
public function prefs_save($args)
{
if ($args['section'] != 'addressbook') {
return $args;
}
// Check that configuration is not disabled
$dont_override = (array) $this->rc->config->get('dont_override', array());
$key = 'kolab_addressbook_prio';
if (!in_array('kolab_addressbook_prio', $dont_override) || !isset($_POST['_'.$key])) {
$args['prefs'][$key] = (int) rcube_utils::get_input_value('_'.$key, rcube_utils::INPUT_POST);
}
return $args;
}
/**
* Handler for plugin actions
*/
public function book_actions()
{
$action = trim(rcube_utils::get_input_value('_act', rcube_utils::INPUT_GPC));
if ($action == 'create') {
$this->ui->book_edit();
}
else if ($action == 'edit') {
$this->ui->book_edit();
}
else if ($action == 'delete') {
$this->book_delete();
}
}
/**
* Handler for address book create/edit form submit
*/
public function book_save()
{
$prop = array(
'name' => trim(rcube_utils::get_input_value('_name', rcube_utils::INPUT_POST)),
'oldname' => trim(rcube_utils::get_input_value('_oldname', rcube_utils::INPUT_POST, true)), // UTF7-IMAP
'parent' => trim(rcube_utils::get_input_value('_parent', rcube_utils::INPUT_POST, true)), // UTF7-IMAP
'type' => 'contact',
'subscribed' => true,
);
$result = $error = false;
$type = strlen($prop['oldname']) ? 'update' : 'create';
$prop = $this->rc->plugins->exec_hook('addressbook_'.$type, $prop);
if (!$prop['abort']) {
if ($newfolder = kolab_storage::folder_update($prop)) {
$folder = $newfolder;
$result = true;
}
else {
$error = kolab_storage::$last_error;
}
}
else {
$result = $prop['result'];
$folder = $prop['name'];
}
if ($result) {
$kolab_folder = kolab_storage::get_folder($folder);
// get folder/addressbook properties
$abook = new rcube_kolab_contacts($folder);
$props = $this->abook_prop(kolab_storage::folder_id($folder, true), $abook);
$props['parent'] = kolab_storage::folder_id($kolab_folder->get_parent(), true);
$this->rc->output->show_message('kolab_addressbook.book'.$type.'d', 'confirmation');
$this->rc->output->command('book_update', $props, kolab_storage::folder_id($prop['oldname'], true));
}
else {
if (!$error) {
$error = $plugin['message'] ? $plugin['message'] : 'kolab_addressbook.book'.$type.'error';
}
$this->rc->output->show_message($error, 'error');
}
$this->rc->output->send('iframe');
}
/**
*
*/
public function book_search()
{
$results = array();
$query = rcube_utils::get_input_value('q', rcube_utils::INPUT_GPC);
$source = rcube_utils::get_input_value('source', rcube_utils::INPUT_GPC);
kolab_storage::$encode_ids = true;
$search_more_results = false;
$this->sources = array();
$this->folders = array();
// find unsubscribed IMAP folders that have "event" type
if ($source == 'folders') {
foreach ((array)kolab_storage::search_folders('contact', $query, array('other')) as $folder) {
$this->folders[$folder->id] = $folder;
$this->sources[$folder->id] = new rcube_kolab_contacts($folder->name);
}
}
// search other user's namespace via LDAP
else if ($source == 'users') {
$limit = $this->rc->config->get('autocomplete_max', 15) * 2; // we have slightly more space, so display twice the number
foreach (kolab_storage::search_users($query, 0, array(), $limit * 10) as $user) {
$folders = array();
// search for contact folders shared by this user
foreach (kolab_storage::list_user_folders($user, 'contact', false) as $foldername) {
$folders[] = new kolab_storage_folder($foldername, 'contact');
}
if (count($folders)) {
$userfolder = new kolab_storage_folder_user($user['kolabtargetfolder'], '', $user);
$this->folders[$userfolder->id] = $userfolder;
$this->sources[$userfolder->id] = $userfolder;
foreach ($folders as $folder) {
$this->folders[$folder->id] = $folder;
$this->sources[$folder->id] = new rcube_kolab_contacts($folder->name);;
$count++;
}
}
if ($count >= $limit) {
$search_more_results = true;
break;
}
}
}
$delim = $this->rc->get_storage()->get_hierarchy_delimiter();
// build results list
foreach ($this->sources as $id => $source) {
$folder = $this->folders[$id];
$imap_path = explode($delim, $folder->name);
// find parent
do {
array_pop($imap_path);
$parent_id = kolab_storage::folder_id(join($delim, $imap_path));
}
while (count($imap_path) > 1 && !$this->folders[$parent_id]);
// restore "real" parent ID
if ($parent_id && !$this->folders[$parent_id]) {
$parent_id = kolab_storage::folder_id($folder->get_parent());
}
$prop = $this->abook_prop($id, $source);
$prop['parent'] = $parent_id;
$html = $this->addressbook_list_item($id, $prop, $jsdata, true);
unset($prop['group']);
$prop += (array)$jsdata[$id];
$prop['html'] = $html;
$results[] = $prop;
}
// report more results available
if ($search_more_results) {
$this->rc->output->show_message('autocompletemore', 'notice');
}
$this->rc->output->command('multi_thread_http_response', $results, rcube_utils::get_input_value('_reqid', rcube_utils::INPUT_GPC));
}
/**
*
*/
public function book_subscribe()
{
$success = false;
$id = rcube_utils::get_input_value('_source', rcube_utils::INPUT_GPC);
if ($id && ($folder = kolab_storage::get_folder(kolab_storage::id_decode($id)))) {
if (isset($_POST['_permanent']))
$success |= $folder->subscribe(intval($_POST['_permanent']));
if (isset($_POST['_active']))
$success |= $folder->activate(intval($_POST['_active']));
// list groups for this address book
if (!empty($_POST['_groups'])) {
$abook = new rcube_kolab_contacts($folder->name);
foreach ((array)$abook->list_groups() as $prop) {
$prop['source'] = $id;
$prop['id'] = $prop['ID'];
unset($prop['ID']);
$this->rc->output->command('insert_contact_group', $prop);
}
}
}
if ($success) {
$this->rc->output->show_message('successfullysaved', 'confirmation');
}
else {
$this->rc->output->show_message($this->gettext('errorsaving'), 'error');
}
$this->rc->output->send();
}
/**
* Handler for address book delete action (AJAX)
*/
private function book_delete()
{
$folder = trim(rcube_utils::get_input_value('_source', rcube_utils::INPUT_GPC, true, 'UTF7-IMAP'));
if (kolab_storage::folder_delete($folder)) {
$storage = $this->rc->get_storage();
$delimiter = $storage->get_hierarchy_delimiter();
$this->rc->output->show_message('kolab_addressbook.bookdeleted', 'confirmation');
$this->rc->output->set_env('pagecount', 0);
$this->rc->output->command('set_rowcount', rcmail_get_rowcount_text(new rcube_result_set()));
$this->rc->output->command('set_env', 'delimiter', $delimiter);
$this->rc->output->command('list_contacts_clear');
$this->rc->output->command('book_delete_done', kolab_storage::folder_id($folder, true));
}
else {
$this->rc->output->show_message('kolab_addressbook.bookdeleteerror', 'error');
}
$this->rc->output->send();
}
/**
* Returns value of kolab_addressbook_prio setting
*/
private function addressbook_prio()
{
// Load configuration
if (!$this->config_loaded) {
$this->load_config();
$this->config_loaded = true;
}
$abook_prio = (int) $this->rc->config->get('kolab_addressbook_prio');
// Make sure any global addressbooks are defined
if ($abook_prio == 0 || $abook_prio == 2) {
$ldap_public = $this->rc->config->get('ldap_public');
if (empty($ldap_public)) {
$abook_prio = 1;
}
}
return $abook_prio;
}
/**
* Hook for (contact) folder deletion
*/
function prefs_folder_delete($args)
{
// ignore...
if ($args['abort'] && !$args['result']) {
return $args;
}
$this->_contact_folder_rename($args['name'], false);
}
/**
* Hook for (contact) folder renaming
*/
function prefs_folder_rename($args)
{
// ignore...
if ($args['abort'] && !$args['result']) {
return $args;
}
$this->_contact_folder_rename($args['oldname'], $args['newname']);
}
/**
* Hook for (contact) folder updates. Forward to folder_rename handler if name was changed
*/
function prefs_folder_update($args)
{
// ignore...
if ($args['abort'] && !$args['result']) {
return $args;
}
if ($args['record']['name'] != $args['record']['oldname']) {
$this->_contact_folder_rename($args['record']['oldname'], $args['record']['name']);
}
}
/**
* Apply folder renaming or deletion to the registered birthday calendar address books
*/
private function _contact_folder_rename($oldname, $newname = false)
{
$update = false;
$delimiter = $this->rc->get_storage()->get_hierarchy_delimiter();
$bday_addressbooks = (array)$this->rc->config->get('calendar_birthday_adressbooks', array());
foreach ($bday_addressbooks as $i => $id) {
$folder_name = kolab_storage::id_decode($id);
if ($oldname === $folder_name || strpos($folder_name, $oldname.$delimiter) === 0) {
if ($newname) { // rename
$new_folder = $newname . substr($folder_name, strlen($oldname));
$bday_addressbooks[$i] = kolab_storage::id_encode($new_folder);
}
else { // delete
unset($bday_addressbooks[$i]);
}
$update = true;
}
}
if ($update) {
$this->rc->user->save_prefs(array('calendar_birthday_adressbooks' => $bday_addressbooks));
}
}
}
diff --git a/plugins/libkolab/SQL/mysql.initial.sql b/plugins/libkolab/SQL/mysql.initial.sql
index f56486f4..f3344e6a 100644
--- a/plugins/libkolab/SQL/mysql.initial.sql
+++ b/plugins/libkolab/SQL/mysql.initial.sql
@@ -1,199 +1,222 @@
/**
* libkolab database schema
*
* @author Thomas Bruederli
* @licence GNU AGPL
*/
SET FOREIGN_KEY_CHECKS=0;
DROP TABLE IF EXISTS `kolab_folders`;
CREATE TABLE `kolab_folders` (
`folder_id` BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
`resource` VARCHAR(255) BINARY NOT NULL,
`type` VARCHAR(32) NOT NULL,
`synclock` INT(10) NOT NULL DEFAULT '0',
`ctag` VARCHAR(40) DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
`objectcount` BIGINT DEFAULT NULL,
PRIMARY KEY(`folder_id`),
INDEX `resource_type` (`resource`, `type`)
) ROW_FORMAT=DYNAMIC ENGINE=INNODB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
DROP TABLE IF EXISTS `kolab_cache`;
DROP TABLE IF EXISTS `kolab_cache_contact`;
CREATE TABLE `kolab_cache_contact` (
`folder_id` BIGINT UNSIGNED NOT NULL,
`msguid` BIGINT UNSIGNED NOT NULL,
`uid` VARCHAR(512) NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
`data` LONGTEXT NOT NULL,
`tags` TEXT NOT NULL,
`words` TEXT NOT NULL,
`type` VARCHAR(32) CHARACTER SET ascii NOT NULL,
`name` VARCHAR(255) NOT NULL,
`firstname` VARCHAR(255) NOT NULL,
`surname` VARCHAR(255) NOT NULL,
`email` VARCHAR(255) NOT NULL,
CONSTRAINT `fk_kolab_cache_contact_folder` FOREIGN KEY (`folder_id`)
REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY(`folder_id`,`msguid`),
INDEX `contact_type` (`folder_id`,`type`),
INDEX `contact_uid2msguid` (`folder_id`,`uid`,`msguid`)
) ROW_FORMAT=DYNAMIC ENGINE=INNODB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
DROP TABLE IF EXISTS `kolab_cache_event`;
CREATE TABLE `kolab_cache_event` (
`folder_id` BIGINT UNSIGNED NOT NULL,
`msguid` BIGINT UNSIGNED NOT NULL,
`uid` VARCHAR(512) NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
`data` LONGTEXT NOT NULL,
`tags` TEXT NOT NULL,
`words` TEXT NOT NULL,
`dtstart` DATETIME,
`dtend` DATETIME,
CONSTRAINT `fk_kolab_cache_event_folder` FOREIGN KEY (`folder_id`)
REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY(`folder_id`,`msguid`),
INDEX `event_uid2msguid` (`folder_id`,`uid`,`msguid`)
) ROW_FORMAT=DYNAMIC ENGINE=INNODB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
DROP TABLE IF EXISTS `kolab_cache_task`;
CREATE TABLE `kolab_cache_task` (
`folder_id` BIGINT UNSIGNED NOT NULL,
`msguid` BIGINT UNSIGNED NOT NULL,
`uid` VARCHAR(512) NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
`data` LONGTEXT NOT NULL,
`tags` TEXT NOT NULL,
`words` TEXT NOT NULL,
`dtstart` DATETIME,
`dtend` DATETIME,
CONSTRAINT `fk_kolab_cache_task_folder` FOREIGN KEY (`folder_id`)
REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY(`folder_id`,`msguid`),
INDEX `task_uid2msguid` (`folder_id`,`uid`,`msguid`)
) ROW_FORMAT=DYNAMIC ENGINE=INNODB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
DROP TABLE IF EXISTS `kolab_cache_journal`;
CREATE TABLE `kolab_cache_journal` (
`folder_id` BIGINT UNSIGNED NOT NULL,
`msguid` BIGINT UNSIGNED NOT NULL,
`uid` VARCHAR(512) NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
`data` LONGTEXT NOT NULL,
`tags` TEXT NOT NULL,
`words` TEXT NOT NULL,
`dtstart` DATETIME,
`dtend` DATETIME,
CONSTRAINT `fk_kolab_cache_journal_folder` FOREIGN KEY (`folder_id`)
REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY(`folder_id`,`msguid`),
INDEX `journal_uid2msguid` (`folder_id`,`uid`,`msguid`)
) ROW_FORMAT=DYNAMIC ENGINE=INNODB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
DROP TABLE IF EXISTS `kolab_cache_note`;
CREATE TABLE `kolab_cache_note` (
`folder_id` BIGINT UNSIGNED NOT NULL,
`msguid` BIGINT UNSIGNED NOT NULL,
`uid` VARCHAR(512) NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
`data` LONGTEXT NOT NULL,
`tags` TEXT NOT NULL,
`words` TEXT NOT NULL,
CONSTRAINT `fk_kolab_cache_note_folder` FOREIGN KEY (`folder_id`)
REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY(`folder_id`,`msguid`),
INDEX `note_uid2msguid` (`folder_id`,`uid`,`msguid`)
) ROW_FORMAT=DYNAMIC ENGINE=INNODB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
DROP TABLE IF EXISTS `kolab_cache_file`;
CREATE TABLE `kolab_cache_file` (
`folder_id` BIGINT UNSIGNED NOT NULL,
`msguid` BIGINT UNSIGNED NOT NULL,
`uid` VARCHAR(512) NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
`data` LONGTEXT NOT NULL,
`tags` TEXT NOT NULL,
`words` TEXT NOT NULL,
`filename` varchar(255) DEFAULT NULL,
CONSTRAINT `fk_kolab_cache_file_folder` FOREIGN KEY (`folder_id`)
REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY(`folder_id`,`msguid`),
INDEX `folder_filename` (`folder_id`, `filename`),
INDEX `file_uid2msguid` (`folder_id`,`uid`,`msguid`)
) ROW_FORMAT=DYNAMIC ENGINE=INNODB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
DROP TABLE IF EXISTS `kolab_cache_configuration`;
CREATE TABLE `kolab_cache_configuration` (
`folder_id` BIGINT UNSIGNED NOT NULL,
`msguid` BIGINT UNSIGNED NOT NULL,
`uid` VARCHAR(512) NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
`data` LONGTEXT NOT NULL,
`tags` TEXT NOT NULL,
`words` TEXT NOT NULL,
`type` VARCHAR(32) CHARACTER SET ascii NOT NULL,
CONSTRAINT `fk_kolab_cache_configuration_folder` FOREIGN KEY (`folder_id`)
REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY(`folder_id`,`msguid`),
INDEX `configuration_type` (`folder_id`,`type`),
INDEX `configuration_uid2msguid` (`folder_id`,`uid`,`msguid`)
) ROW_FORMAT=DYNAMIC ENGINE=INNODB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
DROP TABLE IF EXISTS `kolab_cache_freebusy`;
CREATE TABLE `kolab_cache_freebusy` (
`folder_id` BIGINT UNSIGNED NOT NULL,
`msguid` BIGINT UNSIGNED NOT NULL,
`uid` VARCHAR(512) NOT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
`data` LONGTEXT NOT NULL,
`tags` TEXT NOT NULL,
`words` TEXT NOT NULL,
`dtstart` DATETIME,
`dtend` DATETIME,
CONSTRAINT `fk_kolab_cache_freebusy_folder` FOREIGN KEY (`folder_id`)
REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY(`folder_id`,`msguid`),
INDEX `freebusy_uid2msguid` (`folder_id`,`uid`,`msguid`)
) ROW_FORMAT=DYNAMIC ENGINE=INNODB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
+DROP TABLE IF EXISTS `kolab_cache_dav_contact`;
+
+CREATE TABLE `kolab_cache_dav_contact` (
+ `folder_id` BIGINT UNSIGNED NOT NULL,
+ `uid` VARCHAR(512) NOT NULL,
+ `etag` VARCHAR(128) DEFAULT NULL,
+ `created` DATETIME DEFAULT NULL,
+ `changed` DATETIME DEFAULT NULL,
+ `data` LONGTEXT NOT NULL,
+ `tags` TEXT NOT NULL,
+ `words` TEXT NOT NULL,
+ `type` VARCHAR(32) CHARACTER SET ascii NOT NULL,
+ `name` VARCHAR(255) NOT NULL,
+ `firstname` VARCHAR(255) NOT NULL,
+ `surname` VARCHAR(255) NOT NULL,
+ `email` VARCHAR(255) NOT NULL,
+ CONSTRAINT `fk_kolab_cache_dav_contact_folder` FOREIGN KEY (`folder_id`)
+ REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
+ PRIMARY KEY(`folder_id`,`uid`),
+ INDEX `contact_type` (`folder_id`,`type`)
+) ROW_FORMAT=DYNAMIC ENGINE=INNODB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
+
DROP TABLE IF EXISTS `kolab_cache_dav_event`;
CREATE TABLE `kolab_cache_dav_event` (
`folder_id` BIGINT UNSIGNED NOT NULL,
`uid` VARCHAR(512) NOT NULL,
+ `etag` VARCHAR(128) DEFAULT NULL,
`created` DATETIME DEFAULT NULL,
`changed` DATETIME DEFAULT NULL,
`data` LONGTEXT NOT NULL,
`tags` TEXT NOT NULL,
`words` TEXT NOT NULL,
`dtstart` DATETIME,
`dtend` DATETIME,
CONSTRAINT `fk_kolab_cache_dav_event_folder` FOREIGN KEY (`folder_id`)
REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
PRIMARY KEY(`folder_id`,`uid`)
) ROW_FORMAT=DYNAMIC ENGINE=INNODB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
SET FOREIGN_KEY_CHECKS=1;
REPLACE INTO `system` (`name`, `value`) VALUES ('libkolab-version', '2022100500');
diff --git a/plugins/libkolab/SQL/mysql/2022100500.sql b/plugins/libkolab/SQL/mysql/2022100500.sql
new file mode 100644
index 00000000..7c763321
--- /dev/null
+++ b/plugins/libkolab/SQL/mysql/2022100500.sql
@@ -0,0 +1,39 @@
+DROP TABLE IF EXISTS `kolab_cache_dav_contact`;
+
+CREATE TABLE `kolab_cache_dav_contact` (
+ `folder_id` BIGINT UNSIGNED NOT NULL,
+ `uid` VARCHAR(512) NOT NULL,
+ `etag` VARCHAR(128) DEFAULT NULL,
+ `created` DATETIME DEFAULT NULL,
+ `changed` DATETIME DEFAULT NULL,
+ `data` LONGTEXT NOT NULL,
+ `tags` TEXT NOT NULL,
+ `words` TEXT NOT NULL,
+ `type` VARCHAR(32) CHARACTER SET ascii NOT NULL,
+ `name` VARCHAR(255) NOT NULL,
+ `firstname` VARCHAR(255) NOT NULL,
+ `surname` VARCHAR(255) NOT NULL,
+ `email` VARCHAR(255) NOT NULL,
+ CONSTRAINT `fk_kolab_cache_dav_contact_folder` FOREIGN KEY (`folder_id`)
+ REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
+ PRIMARY KEY(`folder_id`,`uid`),
+ INDEX `contact_type` (`folder_id`,`type`)
+) ROW_FORMAT=DYNAMIC ENGINE=INNODB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
+
+DROP TABLE IF EXISTS `kolab_cache_dav_event`;
+
+CREATE TABLE `kolab_cache_dav_event` (
+ `folder_id` BIGINT UNSIGNED NOT NULL,
+ `uid` VARCHAR(512) NOT NULL,
+ `etag` VARCHAR(128) DEFAULT NULL,
+ `created` DATETIME DEFAULT NULL,
+ `changed` DATETIME DEFAULT NULL,
+ `data` LONGTEXT NOT NULL,
+ `tags` TEXT NOT NULL,
+ `words` TEXT NOT NULL,
+ `dtstart` DATETIME,
+ `dtend` DATETIME,
+ CONSTRAINT `fk_kolab_cache_dav_event_folder` FOREIGN KEY (`folder_id`)
+ REFERENCES `kolab_folders`(`folder_id`) ON DELETE CASCADE ON UPDATE CASCADE,
+ PRIMARY KEY(`folder_id`,`uid`)
+) ROW_FORMAT=DYNAMIC ENGINE=INNODB CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
diff --git a/plugins/libkolab/lib/kolab_dav_client.php b/plugins/libkolab/lib/kolab_dav_client.php
index dba20edc..07a880d9 100644
--- a/plugins/libkolab/lib/kolab_dav_client.php
+++ b/plugins/libkolab/lib/kolab_dav_client.php
@@ -1,450 +1,494 @@
<?php
/**
* A *DAV client.
*
* @author Aleksander Machniak <machniak@apheleia-it.ch>
*
* Copyright (C) 2022, Apheleia IT AG <contact@apheleia-it.ch>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_dav_client
{
public $url;
protected $rc;
protected $responseHeaders = [];
/**
* Object constructor
*/
public function __construct($url)
{
$this->url = $url;
$this->rc = rcube::get_instance();
}
/**
* Execute HTTP request to a DAV server
*/
protected function request($path, $method, $body = '', $headers = [])
{
$rcube = rcube::get_instance();
$debug = (array) $rcube->config->get('dav_debug');
$request_config = [
'store_body' => true,
'follow_redirects' => true,
];
$this->responseHeaders = [];
if ($path && ($rootPath = parse_url($this->url, PHP_URL_PATH)) && strpos($path, $rootPath) === 0) {
$path = substr($path, strlen($rootPath));
}
try {
$request = $this->initRequest($this->url . $path, $method, $request_config);
$request->setAuth($this->rc->user->get_username(), $this->rc->decrypt($_SESSION['password']));
if ($body) {
$request->setBody($body);
$request->setHeader(['Content-Type' => 'application/xml; charset=utf-8']);
}
if (!empty($headers)) {
$request->setHeader($headers);
}
if ($debug) {
rcube::write_log('dav', "C: {$method}: " . (string) $request->getUrl()
. "\n" . $this->debugBody($body, $request->getHeaders()));
}
$response = $request->send();
$body = $response->getBody();
$code = $response->getStatus();
if ($debug) {
rcube::write_log('dav', "S: [{$code}]\n" . $this->debugBody($body, $response->getHeader()));
}
if ($code >= 300) {
throw new Exception("DAV Error ($code):\n{$body}");
}
$this->responseHeaders = $response->getHeader();
return $this->parseXML($body);
}
catch (Exception $e) {
rcube::raise_error($e, true, false);
return false;
}
}
/**
* Discover DAV folders of specified type on the server
*/
public function discover($component = 'VEVENT')
{
/*
$path = parse_url($this->url, PHP_URL_PATH);
$body = '<?xml version="1.0" encoding="utf-8"?>'
. '<d:propfind xmlns:d="DAV:">'
. '<d:prop>'
. '<d:current-user-principal />'
. '</d:prop>'
. '</d:propfind>';
$response = $this->request('/calendars', 'PROPFIND', $body);
$elements = $response->getElementsByTagName('response');
foreach ($elements as $element) {
foreach ($element->getElementsByTagName('prop') as $prop) {
$principal_href = $prop->nodeValue;
break;
}
}
if ($path && strpos($principal_href, $path) === 0) {
$principal_href = substr($principal_href, strlen($path));
}
$body = '<?xml version="1.0" encoding="utf-8"?>'
. '<d:propfind xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">'
. '<d:prop>'
. '<c:calendar-home-set />'
. '</d:prop>'
. '</d:propfind>';
$response = $this->request($principal_href, 'PROPFIND', $body);
*/
$roots = [
'VEVENT' => 'calendars',
'VTODO' => 'calendars',
'VCARD' => 'addressbooks',
];
$principal_href = '/' . $roots[$component] . '/' . $this->rc->user->get_username();
$body = '<?xml version="1.0" encoding="utf-8"?>'
. '<d:propfind xmlns:d="DAV:" xmlns:cs="http://calendarserver.org/ns/" xmlns:c="urn:ietf:params:xml:ns:caldav" xmlns:a="http://apple.com/ns/ical/">'
. '<d:prop>'
. '<d:resourcetype />'
. '<d:displayname />'
. '<cs:getctag />'
. '<c:supported-calendar-component-set />'
. '<a:calendar-color />'
. '</d:prop>'
. '</d:propfind>';
$response = $this->request($principal_href, 'PROPFIND', $body);
if (empty($response)) {
return false;
}
$folders = [];
foreach ($response->getElementsByTagName('response') as $element) {
$folder = $this->getFolderPropertiesFromResponse($element);
if ($folder['type'] === $component) {
$folders[] = $folder;
}
}
return $folders;
}
/**
- * Create DAV object in a folder
+ * Create a DAV object in a folder
*/
public function create($location, $content)
{
$response = $this->request($location, 'PUT', $content, ['Content-Type' => 'text/calendar; charset=utf-8']);
if ($response !== false) {
$etag = $this->responseHeaders['etag'];
if (preg_match('|^".*"$|', $etag)) {
$etag = substr($etag, 1, -1);
}
return $etag;
}
return false;
}
/**
- * Delete DAV object from a folder
+ * Update a DAV object in a folder
+ */
+ public function update($location, $content)
+ {
+ return $this->create($location, $content);
+ }
+
+ /**
+ * Delete a DAV object from a folder
*/
public function delete($location)
{
$response = $this->request($location, 'DELETE', '', ['Depth' => 1, 'Prefer' => 'return-minimal']);
return $response !== false;
}
/**
* Fetch DAV objects metadata (ETag, href) a folder
*/
public function getIndex($location, $component = 'VEVENT')
{
+ $queries = [
+ 'VEVENT' => 'calendar-query',
+ 'VTODO' => 'calendar-query',
+ 'VCARD' => 'addressbook-query',
+ ];
+
+ $ns = [
+ 'VEVENT' => 'caldav',
+ 'VTODO' => 'caldav',
+ 'VCARD' => 'carddav',
+ ];
+
+ $filter = '';
+ if ($component != 'VCARD') {
+ $filter = '<c:comp-filter name="VCALENDAR">'
+ . '<c:comp-filter name="' . $component . '" />'
+ . '</c:comp-filter>';
+ }
+
$body = '<?xml version="1.0" encoding="utf-8"?>'
- .' <c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">'
+ .' <c:' . $queries[$component] . ' xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:' . $ns[$component]. '">'
. '<d:prop>'
. '<d:getetag />'
. '</d:prop>'
- . '<c:filter>'
- . '<c:comp-filter name="VCALENDAR">'
- . '<c:comp-filter name="' . $component . '" />'
- . '</c:comp-filter>'
- . '</c:filter>'
- . '</c:calendar-query>';
+ . ($filter ? "<c:filter>$filter</c:filter>" : '')
+ . '</c:' . $queries[$component] . '>';
$response = $this->request($location, 'REPORT', $body, ['Depth' => 1, 'Prefer' => 'return-minimal']);
if (empty($response)) {
return false;
}
$objects = [];
foreach ($response->getElementsByTagName('response') as $element) {
$objects[] = $this->getObjectPropertiesFromResponse($element);
}
return $objects;
}
/**
* Fetch DAV objects data from a folder
*/
- public function getData($location, $hrefs = [])
+ public function getData($location, $component = 'VEVENT', $hrefs = [])
{
if (empty($hrefs)) {
return [];
}
$body = '';
foreach ($hrefs as $href) {
$body .= '<d:href>' . $href . '</d:href>';
}
+ $queries = [
+ 'VEVENT' => 'calendar-multiget',
+ 'VTODO' => 'calendar-multiget',
+ 'VCARD' => 'addressbook-multiget',
+ ];
+
+ $ns = [
+ 'VEVENT' => 'caldav',
+ 'VTODO' => 'caldav',
+ 'VCARD' => 'carddav',
+ ];
+
+ $types = [
+ 'VEVENT' => 'calendar-data',
+ 'VTODO' => 'calendar-data',
+ 'VCARD' => 'address-data',
+ ];
+
$body = '<?xml version="1.0" encoding="utf-8"?>'
- .' <c:calendar-multiget xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">'
+ .' <c:' . $queries[$component] . ' xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:' . $ns[$component] . '">'
. '<d:prop>'
. '<d:getetag />'
- . '<c:calendar-data />'
+ . '<c:' . $types[$component]. ' />'
. '</d:prop>'
. $body
- . '</c:calendar-multiget>';
+ . '</c:' . $queries[$component] . '>';
$response = $this->request($location, 'REPORT', $body, ['Depth' => 1, 'Prefer' => 'return-minimal']);
if (empty($response)) {
return false;
}
$objects = [];
foreach ($response->getElementsByTagName('response') as $element) {
$objects[] = $this->getObjectPropertiesFromResponse($element);
}
return $objects;
}
/**
* Parse XML content
*/
protected function parseXML($xml)
{
$doc = new DOMDocument('1.0', 'UTF-8');
if (stripos($xml, '<?xml') === 0) {
if (!$doc->loadXML($xml)) {
throw new Exception("Failed to parse XML");
}
$doc->formatOutput = true;
}
return $doc;
}
/**
* Parse request/response body for debug purposes
*/
protected function debugBody($body, $headers)
{
$head = '';
foreach ($headers as $header_name => $header_value) {
$head .= "{$header_name}: {$header_value}\n";
}
if (stripos($body, '<?xml') === 0) {
$doc = new DOMDocument('1.0', 'UTF-8');
if (!$doc->loadXML($body)) {
throw new Exception("Failed to parse XML");
}
$doc->formatOutput = true;
$body = $doc->saveXML();
}
return $head . "\n" . rtrim($body);
}
/**
* Extract folder properties from a server 'response' element
*/
protected function getFolderPropertiesFromResponse(DOMNode $element)
{
if ($href = $element->getElementsByTagName('href')->item(0)) {
$href = $href->nodeValue;
/*
$path = parse_url($this->url, PHP_URL_PATH);
if ($path && strpos($href, $path) === 0) {
$href = substr($href, strlen($path));
}
*/
}
if ($color = $element->getElementsByTagName('calendar-color')->item(0)) {
if (preg_match('/^#[0-9A-F]{8}$/', $color->nodeValue)) {
$color = substr($color->nodeValue, 1, -2);
} else {
$color = null;
}
}
if ($name = $element->getElementsByTagName('displayname')->item(0)) {
$name = $name->nodeValue;
}
if ($ctag = $element->getElementsByTagName('getctag')->item(0)) {
$ctag = $ctag->nodeValue;
}
$component = null;
if ($set_element = $element->getElementsByTagName('supported-calendar-component-set')->item(0)) {
if ($comp_element = $set_element->getElementsByTagName('comp')->item(0)) {
$component = $comp_element->attributes->getNamedItem('name')->nodeValue;
}
}
return [
'href' => $href,
'name' => $name,
'ctag' => $ctag,
'color' => $color,
'type' => $component,
];
}
/**
* Extract object properties from a server 'response' element
*/
protected function getObjectPropertiesFromResponse(DOMNode $element)
{
$uid = null;
if ($href = $element->getElementsByTagName('href')->item(0)) {
$href = $href->nodeValue;
/*
$path = parse_url($this->url, PHP_URL_PATH);
if ($path && strpos($href, $path) === 0) {
$href = substr($href, strlen($path));
}
*/
// Extract UID from the URL
$href_parts = explode('/', $href);
$uid = preg_replace('/\.[a-z]+$/', '', $href_parts[count($href_parts)-1]);
}
if ($data = $element->getElementsByTagName('calendar-data')->item(0)) {
$data = $data->nodeValue;
}
+ else if ($data = $element->getElementsByTagName('address-data')->item(0)) {
+ $data = $data->nodeValue;
+ }
if ($etag = $element->getElementsByTagName('getetag')->item(0)) {
$etag = $etag->nodeValue;
if (preg_match('|^".*"$|', $etag)) {
$etag = substr($etag, 1, -1);
}
}
return [
'href' => $href,
'data' => $data,
'etag' => $etag,
'uid' => $uid,
];
}
/**
* Initialize HTTP request object
*/
protected function initRequest($url = '', $method = 'GET', $config = array())
{
$rcube = rcube::get_instance();
$http_config = (array) $rcube->config->get('kolab_http_request');
// deprecated configuration options
if (empty($http_config)) {
foreach (array('ssl_verify_peer', 'ssl_verify_host') as $option) {
$value = $rcube->config->get('kolab_' . $option, true);
if (is_bool($value)) {
$http_config[$option] = $value;
}
}
}
if (!empty($config)) {
$http_config = array_merge($http_config, $config);
}
// load HTTP_Request2
require_once 'HTTP/Request2.php';
try {
$request = new HTTP_Request2();
$request->setConfig($http_config);
// proxy User-Agent string
$request->setHeader('user-agent', $_SERVER['HTTP_USER_AGENT']);
// cleanup
$request->setBody('');
$request->setUrl($url);
$request->setMethod($method);
return $request;
}
catch (Exception $e) {
rcube::raise_error($e, true, true);
}
}
}
diff --git a/plugins/libkolab/lib/kolab_storage.php b/plugins/libkolab/lib/kolab_storage.php
index b6f773f9..381afe54 100644
--- a/plugins/libkolab/lib/kolab_storage.php
+++ b/plugins/libkolab/lib/kolab_storage.php
@@ -1,1795 +1,1795 @@
<?php
/**
* Kolab storage class providing static methods to access groupware objects on a Kolab server.
*
* @version @package_version@
* @author Thomas Bruederli <bruederli@kolabsys.com>
* @author Aleksander Machniak <machniak@kolabsys.com>
*
* Copyright (C) 2012-2014, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_storage
{
const CTYPE_KEY = '/shared/vendor/kolab/folder-type';
const CTYPE_KEY_PRIVATE = '/private/vendor/kolab/folder-type';
const COLOR_KEY_SHARED = '/shared/vendor/kolab/color';
const COLOR_KEY_PRIVATE = '/private/vendor/kolab/color';
const NAME_KEY_SHARED = '/shared/vendor/kolab/displayname';
const NAME_KEY_PRIVATE = '/private/vendor/kolab/displayname';
const UID_KEY_SHARED = '/shared/vendor/kolab/uniqueid';
const UID_KEY_CYRUS = '/shared/vendor/cmu/cyrus-imapd/uniqueid';
const ERROR_IMAP_CONN = 1;
const ERROR_CACHE_DB = 2;
const ERROR_NO_PERMISSION = 3;
const ERROR_INVALID_FOLDER = 4;
public static $version = '3.0';
public static $last_error;
public static $encode_ids = false;
private static $ready = false;
private static $with_tempsubs = true;
private static $subscriptions;
private static $ldapcache = array();
private static $ldap = array();
private static $states;
private static $config;
private static $imap;
// Default folder names
private static $default_folders = array(
'event' => 'Calendar',
'contact' => 'Contacts',
'task' => 'Tasks',
'note' => 'Notes',
'file' => 'Files',
'configuration' => 'Configuration',
'journal' => 'Journal',
'mail.inbox' => 'INBOX',
'mail.drafts' => 'Drafts',
'mail.sentitems' => 'Sent',
'mail.wastebasket' => 'Trash',
'mail.outbox' => 'Outbox',
'mail.junkemail' => 'Junk',
);
/**
* Setup the environment needed by the libs
*/
public static function setup()
{
if (self::$ready)
return true;
$rcmail = rcube::get_instance();
self::$config = $rcmail->config;
self::$version = strval($rcmail->config->get('kolab_format_version', self::$version));
self::$imap = $rcmail->get_storage();
self::$ready = class_exists('kolabformat') &&
(self::$imap->get_capability('METADATA') || self::$imap->get_capability('ANNOTATEMORE') || self::$imap->get_capability('ANNOTATEMORE2'));
if (self::$ready) {
// do nothing
}
else if (!class_exists('kolabformat')) {
rcube::raise_error(array(
'code' => 900, 'type' => 'php',
'message' => "required kolabformat module not found"
), true);
}
else if (self::$imap->get_error_code()) {
rcube::raise_error(array(
'code' => 900, 'type' => 'php', 'message' => "IMAP error"
), true);
}
// adjust some configurable settings
if ($event_scheduling_prop = $rcmail->config->get('kolab_event_scheduling_properties', null)) {
kolab_format_event::$scheduling_properties = (array)$event_scheduling_prop;
}
// adjust some configurable settings
if ($task_scheduling_prop = $rcmail->config->get('kolab_task_scheduling_properties', null)) {
kolab_format_task::$scheduling_properties = (array)$task_scheduling_prop;
}
return self::$ready;
}
/**
* Initializes LDAP object to resolve Kolab users
*
* @param string $name Name of the configuration option with LDAP config
*/
public static function ldap($name = 'kolab_users_directory')
{
self::setup();
$config = self::$config->get($name);
if (empty($config)) {
$name = 'kolab_auth_addressbook';
$config = self::$config->get($name);
}
if (self::$ldap[$name]) {
return self::$ldap[$name];
}
if (!is_array($config)) {
$ldap_config = (array)self::$config->get('ldap_public');
$config = $ldap_config[$config];
}
if (empty($config)) {
return null;
}
$ldap = new kolab_ldap($config);
// overwrite filter option
if ($filter = self::$config->get('kolab_users_filter')) {
self::$config->set('kolab_auth_filter', $filter);
}
$user_field = $user_attrib = self::$config->get('kolab_users_id_attrib');
// Fallback to kolab_auth_login, which is not attribute, but field name
if (!$user_field && ($user_field = self::$config->get('kolab_auth_login', 'email'))) {
$user_attrib = $config['fieldmap'][$user_field];
}
if ($user_field && $user_attrib) {
$ldap->extend_fieldmap(array($user_field => $user_attrib));
}
self::$ldap[$name] = $ldap;
return $ldap;
}
/**
* Get a list of storage folders for the given data type
*
* @param string Data type to list folders for (contact,distribution-list,event,task,note)
* @param boolean Enable to return subscribed folders only (null to use configured subscription mode)
*
* @return array List of Kolab_Folder objects (folder names in UTF7-IMAP)
*/
public static function get_folders($type, $subscribed = null)
{
$folders = $folderdata = array();
if (self::setup()) {
foreach ((array)self::list_folders('', '*', $type, $subscribed, $folderdata) as $foldername) {
$folders[$foldername] = new kolab_storage_folder($foldername, $type, $folderdata[$foldername]);
}
}
return $folders;
}
/**
* Getter for the storage folder for the given type
*
* @param string Data type to list folders for (contact,distribution-list,event,task,note)
* @return object kolab_storage_folder The folder object
*/
public static function get_default_folder($type)
{
if (self::setup()) {
foreach ((array)self::list_folders('', '*', $type . '.default', false, $folderdata) as $foldername) {
return new kolab_storage_folder($foldername, $type, $folderdata[$foldername]);
}
}
return null;
}
/**
* Getter for a specific storage folder
*
* @param string IMAP folder to access (UTF7-IMAP)
* @param string Expected folder type
*
* @return object kolab_storage_folder The folder object
*/
public static function get_folder($folder, $type = null)
{
return self::setup() ? new kolab_storage_folder($folder, $type) : null;
}
/**
* Getter for a single Kolab object, identified by its UID.
* This will search all folders storing objects of the given type.
*
* @param string Object UID
* @param string Object type (contact,event,task,journal,file,note,configuration)
* @return array The Kolab object represented as hash array or false if not found
*/
public static function get_object($uid, $type)
{
self::setup();
$folder = null;
foreach ((array)self::list_folders('', '*', $type, null, $folderdata) as $foldername) {
if (!$folder)
$folder = new kolab_storage_folder($foldername, $type, $folderdata[$foldername]);
else
$folder->set_folder($foldername, $type, $folderdata[$foldername]);
if ($object = $folder->get_object($uid))
return $object;
}
return false;
}
/**
* Execute cross-folder searches with the given query.
*
* @param array Pseudo-SQL query as list of filter parameter triplets
* @param string Folder type (contact,event,task,journal,file,note,configuration)
* @param int Expected number of records or limit (for performance reasons)
*
* @return array List of Kolab data objects (each represented as hash array)
* @see kolab_storage_format::select()
*/
public static function select($query, $type, $limit = null)
{
self::setup();
$folder = null;
$result = array();
foreach ((array)self::list_folders('', '*', $type, null, $folderdata) as $foldername) {
$folder = new kolab_storage_folder($foldername, $type, $folderdata[$foldername]);
if ($limit) {
$folder->set_order_and_limit(null, $limit);
}
foreach ($folder->select($query) as $object) {
$result[] = $object;
}
}
return $result;
}
/**
* Returns Free-busy server URL
*/
public static function get_freebusy_server()
{
self::setup();
$url = 'https://' . $_SESSION['imap_host'] . '/freebusy';
$url = self::$config->get('kolab_freebusy_server', $url);
$url = rcube_utils::resolve_url($url);
return unslashify($url);
}
/**
* Compose an URL to query the free/busy status for the given user
*
* @param string Email address of the user to get free/busy data for
* @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
*/
public static function get_freebusy_url($email, $start = null, $end = null)
{
$query = '';
$param = array();
$utc = new \DateTimeZone('UTC');
if ($start instanceof \DateTime) {
$start->setTimezone($utc);
$param['dtstart'] = $start->format('Ymd\THis\Z');
}
if ($end instanceof \DateTime) {
$end->setTimezone($utc);
$param['dtend'] = $end->format('Ymd\THis\Z');
}
if (!empty($param)) {
$query = '?' . http_build_query($param);
}
return self::get_freebusy_server() . '/' . $email . '.ifb' . $query;
}
/**
* Creates folder ID from folder name
*
* @param string $folder Folder name (UTF7-IMAP)
* @param boolean $enc Use lossless encoding
* @return string Folder ID string
*/
public static function folder_id($folder, $enc = null)
{
return $enc == true || ($enc === null && self::$encode_ids) ?
self::id_encode($folder) :
asciiwords(strtr($folder, '/.-', '___'));
}
/**
* Encode the given ID to a safe ascii representation
*
* @param string $id Arbitrary identifier string
*
* @return string Ascii representation
*/
public static function id_encode($id)
{
return rtrim(strtr(base64_encode($id), '+/', '-_'), '=');
}
/**
* Convert the given identifier back to it's raw value
*
* @param string $id Ascii identifier
* @return string Raw identifier string
*/
public static function id_decode($id)
{
return base64_decode(str_pad(strtr($id, '-_', '+/'), strlen($id) % 4, '=', STR_PAD_RIGHT));
}
/**
* Return the (first) path of the requested IMAP namespace
*
* @param string Namespace name (personal, shared, other)
* @return string IMAP root path for that namespace
*/
public static function namespace_root($name)
{
self::setup();
foreach ((array)self::$imap->get_namespace($name) as $paths) {
if (strlen($paths[0]) > 1) {
return $paths[0];
}
}
return '';
}
/**
* Deletes IMAP folder
*
* @param string $name Folder name (UTF7-IMAP)
*
* @return bool True on success, false on failure
*/
public static function folder_delete($name)
{
// clear cached entries first
if ($folder = self::get_folder($name))
$folder->cache->purge();
$rcmail = rcube::get_instance();
$plugin = $rcmail->plugins->exec_hook('folder_delete', array('name' => $name));
$success = self::$imap->delete_folder($name);
self::$last_error = self::$imap->get_error_str();
return $success;
}
/**
* Creates IMAP folder
*
* @param string $name Folder name (UTF7-IMAP)
* @param string $type Folder type
* @param bool $subscribed Sets folder subscription
* @param bool $active Sets folder state (client-side subscription)
*
* @return bool True on success, false on failure
*/
public static function folder_create($name, $type = null, $subscribed = false, $active = false)
{
self::setup();
$rcmail = rcube::get_instance();
$plugin = $rcmail->plugins->exec_hook('folder_create', array('record' => array(
'name' => $name,
'subscribe' => $subscribed,
)));
if ($saved = self::$imap->create_folder($name, $subscribed)) {
// set metadata for folder type
if ($type) {
$saved = self::set_folder_type($name, $type);
// revert if metadata could not be set
if (!$saved) {
self::$imap->delete_folder($name);
}
// activate folder
else if ($active) {
self::set_state($name, true);
}
}
}
if ($saved) {
return true;
}
self::$last_error = self::$imap->get_error_str();
return false;
}
/**
* Renames IMAP folder
*
* @param string $oldname Old folder name (UTF7-IMAP)
* @param string $newname New folder name (UTF7-IMAP)
*
* @return bool True on success, false on failure
*/
public static function folder_rename($oldname, $newname)
{
self::setup();
$rcmail = rcube::get_instance();
$plugin = $rcmail->plugins->exec_hook('folder_rename', array(
'oldname' => $oldname, 'newname' => $newname));
$oldfolder = self::get_folder($oldname);
$active = self::folder_is_active($oldname);
$success = self::$imap->rename_folder($oldname, $newname);
self::$last_error = self::$imap->get_error_str();
// pass active state to new folder name
if ($success && $active) {
self::set_state($oldname, false);
self::set_state($newname, true);
}
// assign existing cache entries to new resource uri
if ($success && $oldfolder) {
$oldfolder->cache->rename($newname);
}
return $success;
}
/**
* Rename or Create a new IMAP folder.
*
* Does additional checks for permissions and folder name restrictions
*
* @param array &$prop Hash array with folder properties and metadata
* - name: Folder name
* - oldname: Old folder name when changed
* - parent: Parent folder to create the new one in
* - type: Folder type to create
* - subscribed: Subscribed flag (IMAP subscription)
* - active: Activation flag (client-side subscription)
*
* @return string|false New folder name or False on failure
*
* @see self::set_folder_props() for list of other properties
*/
public static function folder_update(&$prop)
{
self::setup();
$folder = rcube_charset::convert($prop['name'], RCUBE_CHARSET, 'UTF7-IMAP');
$oldfolder = $prop['oldname']; // UTF7
$parent = $prop['parent']; // UTF7
$delimiter = self::$imap->get_hierarchy_delimiter();
if (strlen($oldfolder)) {
$options = self::$imap->folder_info($oldfolder);
}
if (!empty($options) && ($options['norename'] || $options['protected'])) {
}
// sanity checks (from steps/settings/save_folder.inc)
else if (!strlen($folder)) {
self::$last_error = 'cannotbeempty';
return false;
}
else if (strlen($folder) > 128) {
self::$last_error = 'nametoolong';
return false;
}
else {
// these characters are problematic e.g. when used in LIST/LSUB
foreach (array($delimiter, '%', '*') as $char) {
if (strpos($folder, $char) !== false) {
self::$last_error = 'forbiddencharacter';
return false;
}
}
}
if (!empty($options) && ($options['protected'] || $options['norename'])) {
$folder = $oldfolder;
}
else if (strlen($parent)) {
$folder = $parent . $delimiter . $folder;
}
else {
// add namespace prefix (when needed)
$folder = self::$imap->mod_folder($folder, 'in');
}
// Check access rights to the parent folder
if (strlen($parent) && (!strlen($oldfolder) || $oldfolder != $folder)) {
$parent_opts = self::$imap->folder_info($parent);
if ($parent_opts['namespace'] != 'personal'
&& (empty($parent_opts['rights']) || !preg_match('/[ck]/', implode($parent_opts['rights'])))
) {
self::$last_error = 'No permission to create folder';
return false;
}
}
// update the folder name
if (strlen($oldfolder)) {
if ($oldfolder != $folder) {
$result = self::folder_rename($oldfolder, $folder);
}
else {
$result = true;
}
}
// create new folder
else {
$result = self::folder_create($folder, $prop['type'], $prop['subscribed'], $prop['active']);
}
if ($result) {
self::set_folder_props($folder, $prop);
}
return $result ? $folder : false;
}
/**
* Getter for human-readable name of Kolab object (folder)
* with kolab_custom_display_names support.
* See http://wiki.kolab.org/UI-Concepts/Folder-Listing for reference
*
* @param string $folder IMAP folder name (UTF7-IMAP)
* @param string $folder_ns Will be set to namespace name of the folder
*
* @return string Name of the folder-object
*/
public static function object_name($folder, &$folder_ns=null)
{
// find custom display name in folder METADATA
if ($name = self::custom_displayname($folder)) {
return $name;
}
return self::object_prettyname($folder, $folder_ns);
}
/**
* Get custom display name (saved in metadata) for the given folder
*/
public static function custom_displayname($folder)
{
static $_metadata;
// find custom display name in folder METADATA
if (self::$config->get('kolab_custom_display_names', true) && self::setup()) {
if ($_metadata !== null) {
$metadata = $_metadata;
}
else {
// For performance reasons ask for all folders, it will be cached as one cache entry
$metadata = self::$imap->get_metadata("*", array(self::NAME_KEY_PRIVATE, self::NAME_KEY_SHARED));
// If cache is disabled store result in memory
if (!self::$config->get('imap_cache')) {
$_metadata = $metadata;
}
}
if ($data = $metadata[$folder]) {
if (($name = $data[self::NAME_KEY_PRIVATE]) || ($name = $data[self::NAME_KEY_SHARED])) {
return $name;
}
}
}
return false;
}
/**
* Getter for human-readable name of Kolab object (folder)
* See http://wiki.kolab.org/UI-Concepts/Folder-Listing for reference
*
* @param string $folder IMAP folder name (UTF7-IMAP)
* @param string $folder_ns Will be set to namespace name of the folder
*
* @return string Name of the folder-object
*/
public static function object_prettyname($folder, &$folder_ns=null)
{
self::setup();
$found = false;
$namespace = self::$imap->get_namespace();
if (!empty($namespace['shared'])) {
foreach ($namespace['shared'] as $ns) {
if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
$prefix = '';
$folder = substr($folder, strlen($ns[0]));
$delim = $ns[1];
$found = true;
$folder_ns = 'shared';
break;
}
}
}
if (!$found && !empty($namespace['other'])) {
foreach ($namespace['other'] as $ns) {
if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
// remove namespace prefix and extract username
$folder = substr($folder, strlen($ns[0]));
$delim = $ns[1];
// get username part and map it to user name
$pos = strpos($folder, $delim);
$fid = $pos ? substr($folder, 0, $pos) : $folder;
if ($user = self::folder_id2user($fid, true)) {
$fid = str_replace($delim, '', $user);
}
$prefix = "($fid)";
$folder = $pos ? substr($folder, $pos + 1) : '';
$found = true;
$folder_ns = 'other';
break;
}
}
}
if (!$found && !empty($namespace['personal'])) {
foreach ($namespace['personal'] as $ns) {
if (strlen($ns[0]) && strpos($folder, $ns[0]) === 0) {
// remove namespace prefix
$folder = substr($folder, strlen($ns[0]));
$prefix = '';
$delim = $ns[1];
$found = true;
break;
}
}
}
if (empty($delim))
$delim = self::$imap->get_hierarchy_delimiter();
$folder = rcube_charset::convert($folder, 'UTF7-IMAP');
$folder = html::quote($folder);
$folder = str_replace(html::quote($delim), ' » ', $folder);
if ($prefix)
$folder = html::quote($prefix) . ($folder !== '' ? ' ' . $folder : '');
if (!$folder_ns)
$folder_ns = 'personal';
return $folder;
}
/**
* Helper method to generate a truncated folder name to display.
* Note: $origname is a string returned by self::object_name()
*/
public static function folder_displayname($origname, &$names)
{
$name = $origname;
// find folder prefix to truncate
for ($i = count($names)-1; $i >= 0; $i--) {
if (strpos($name, $names[$i] . ' » ') === 0) {
$length = strlen($names[$i] . ' » ');
$prefix = substr($name, 0, $length);
$count = count(explode(' » ', $prefix));
$diff = 1;
// check if prefix folder is in other users namespace
for ($n = count($names)-1; $n >= 0; $n--) {
if (strpos($prefix, '(' . $names[$n] . ') ') === 0) {
$diff = 0;
break;
}
}
$name = str_repeat(' ', $count - $diff) . '» ' . substr($name, $length);
break;
}
// other users namespace and parent folder exists
else if (strpos($name, '(' . $names[$i] . ') ') === 0) {
$length = strlen('(' . $names[$i] . ') ');
$prefix = substr($name, 0, $length);
$count = count(explode(' » ', $prefix));
$name = str_repeat(' ', $count) . '» ' . substr($name, $length);
break;
}
}
$names[] = $origname;
return $name;
}
/**
* Creates a SELECT field with folders list
*
* @param string $type Folder type
* @param array $attrs SELECT field attributes (e.g. name)
* @param string $current The name of current folder (to skip it)
*
* @return html_select SELECT object
*/
public static function folder_selector($type, $attrs, $current = '')
{
// get all folders of specified type (sorted)
$folders = self::get_folders($type, true);
$delim = self::$imap->get_hierarchy_delimiter();
$names = array();
$len = strlen($current);
if ($len && ($rpos = strrpos($current, $delim))) {
$parent = substr($current, 0, $rpos);
$p_len = strlen($parent);
}
// Filter folders list
foreach ($folders as $c_folder) {
$name = $c_folder->name;
// skip current folder and it's subfolders
if ($len) {
if ($name == $current) {
// Make sure parent folder is listed (might be skipped e.g. if it's namespace root)
if ($p_len && !isset($names[$parent])) {
$names[$parent] = self::object_name($parent);
}
continue;
}
if (strpos($name, $current.$delim) === 0) {
continue;
}
}
// always show the parent of current folder
if ($p_len && $name == $parent) {
}
// skip folders where user have no rights to create subfolders
else if ($c_folder->get_owner() != $_SESSION['username']) {
$rights = $c_folder->get_myrights();
if (!preg_match('/[ck]/', $rights)) {
continue;
}
}
$names[$name] = $c_folder->get_name();
}
// Build SELECT field of parent folder
$attrs['is_escaped'] = true;
$select = new html_select($attrs);
$select->add('---', '');
$listnames = array();
foreach (array_keys($names) as $imap_name) {
$name = $origname = $names[$imap_name];
// find folder prefix to truncate
for ($i = count($listnames)-1; $i >= 0; $i--) {
if (strpos($name, $listnames[$i].' » ') === 0) {
$length = strlen($listnames[$i].' » ');
$prefix = substr($name, 0, $length);
$count = count(explode(' » ', $prefix));
$name = str_repeat(' ', $count-1) . '» ' . substr($name, $length);
break;
}
}
$listnames[] = $origname;
$select->add($name, $imap_name);
}
return $select;
}
/**
* Returns a list of folder names
*
* @param string Optional root folder
* @param string Optional name pattern
* @param string Data type to list folders for (contact,event,task,journal,file,note,mail,configuration)
* @param boolean Enable to return subscribed folders only (null to use configured subscription mode)
* @param array Will be filled with folder-types data
*
* @return array List of folders
*/
public static function list_folders($root = '', $mbox = '*', $filter = null, $subscribed = null, &$folderdata = array())
{
if (!self::setup()) {
return null;
}
// use IMAP subscriptions
if ($subscribed === null && self::$config->get('kolab_use_subscriptions')) {
$subscribed = true;
}
if (!$filter) {
// Get ALL folders list, standard way
if ($subscribed) {
$folders = self::_imap_list_subscribed($root, $mbox);
}
else {
$folders = self::_imap_list_folders($root, $mbox);
}
return $folders;
}
$prefix = $root . $mbox;
$regexp = '/^' . preg_quote($filter, '/') . '(\..+)?$/';
// get folders types for all folders
$folderdata = self::folders_typedata($prefix);
if (!is_array($folderdata)) {
return array();
}
// If we only want groupware folders and don't care about the subscription state,
// then the metadata will already contain all folder names and we can avoid the LIST below.
if (!$subscribed && $filter != 'mail' && $prefix == '*') {
foreach ($folderdata as $folder => $type) {
if (!preg_match($regexp, $type)) {
unset($folderdata[$folder]);
}
}
return self::$imap->sort_folder_list(array_keys($folderdata), true);
}
// Get folders list
if ($subscribed) {
$folders = self::_imap_list_subscribed($root, $mbox);
}
else {
$folders = self::_imap_list_folders($root, $mbox);
}
// In case of an error, return empty list (?)
if (!is_array($folders)) {
return array();
}
// Filter folders list
foreach ($folders as $idx => $folder) {
$type = $folderdata[$folder];
if ($filter == 'mail' && empty($type)) {
continue;
}
if (empty($type) || !preg_match($regexp, $type)) {
unset($folders[$idx]);
}
}
return $folders;
}
/**
* Wrapper for rcube_imap::list_folders() with optional post-filtering
*/
protected static function _imap_list_folders($root, $mbox)
{
$postfilter = null;
// compose a post-filter expression for the excluded namespaces
if ($root . $mbox == '*' && ($skip_ns = self::$config->get('kolab_skip_namespace'))) {
$excludes = array();
foreach ((array)$skip_ns as $ns) {
if ($ns_root = self::namespace_root($ns)) {
$excludes[] = $ns_root;
}
}
if (count($excludes)) {
$postfilter = '!^(' . join(')|(', array_map('preg_quote', $excludes)) . ')!';
}
}
// use normal LIST command to return all folders, it's fast enough
$folders = self::$imap->list_folders($root, $mbox, null, null, !empty($postfilter));
if (!empty($postfilter)) {
$folders = array_filter($folders, function($folder) use ($postfilter) { return !preg_match($postfilter, $folder); });
$folders = self::$imap->sort_folder_list($folders);
}
return $folders;
}
/**
* Wrapper for rcube_imap::list_folders_subscribed()
* with support for temporarily subscribed folders
*/
- protected static function _imap_list_subscribed($root, $mbox)
+ protected static function _imap_list_subscribed($root, $mbox, $filter = null)
{
$folders = self::$imap->list_folders_subscribed($root, $mbox);
// add temporarily subscribed folders
if (self::$with_tempsubs && is_array($_SESSION['kolab_subscribed_folders'])) {
$folders = array_unique(array_merge($folders, $_SESSION['kolab_subscribed_folders']));
}
return $folders;
}
/**
* Search for shared or otherwise not listed groupware folders the user has access
*
* @param string Folder type of folders to search for
* @param string Search string
* @param array Namespace(s) to exclude results from
*
* @return array List of matching kolab_storage_folder objects
*/
public static function search_folders($type, $query, $exclude_ns = array())
{
if (!self::setup()) {
return array();
}
$folders = array();
$query = str_replace('*', '', $query);
// find unsubscribed IMAP folders of the given type
foreach ((array)self::list_folders('', '*', $type, false, $folderdata) as $foldername) {
// FIXME: only consider the last part of the folder path for searching?
$realname = strtolower(rcube_charset::convert($foldername, 'UTF7-IMAP'));
if (($query == '' || strpos($realname, $query) !== false) &&
!self::folder_is_subscribed($foldername, true) &&
!in_array(self::$imap->folder_namespace($foldername), (array)$exclude_ns)
) {
$folders[] = new kolab_storage_folder($foldername, $type, $folderdata[$foldername]);
}
}
return $folders;
}
/**
* Sort the given list of kolab folders by namespace/name
*
* @param array List of kolab_storage_folder objects
* @return array Sorted list of folders
*/
public static function sort_folders($folders)
{
$pad = ' ';
$out = array();
$nsnames = array('personal' => array(), 'shared' => array(), 'other' => array());
foreach ($folders as $folder) {
$_folders[$folder->name] = $folder;
$ns = $folder->get_namespace();
$nsnames[$ns][$folder->name] = strtolower(html_entity_decode($folder->get_name(), ENT_COMPAT, RCUBE_CHARSET)) . $pad; // decode »
}
// $folders is a result of get_folders() we can assume folders were already sorted
foreach (array_keys($nsnames) as $ns) {
asort($nsnames[$ns], SORT_LOCALE_STRING);
foreach (array_keys($nsnames[$ns]) as $utf7name) {
$out[] = $_folders[$utf7name];
}
}
return $out;
}
/**
* Check the folder tree and add the missing parents as virtual folders
*
* @param array $folders Folders list
* @param object $tree Reference to the root node of the folder tree
*
* @return array Flat folders list
*/
public static function folder_hierarchy($folders, &$tree = null)
{
if (!self::setup()) {
return array();
}
$_folders = array();
$delim = self::$imap->get_hierarchy_delimiter();
$other_ns = rtrim(self::namespace_root('other'), $delim);
$tree = new kolab_storage_folder_virtual('', '<root>', ''); // create tree root
$refs = array('' => $tree);
foreach ($folders as $idx => $folder) {
$path = explode($delim, $folder->name);
array_pop($path);
$folder->parent = join($delim, $path);
$folder->children = array(); // reset list
// skip top folders or ones with a custom displayname
if (count($path) < 1 || kolab_storage::custom_displayname($folder->name)) {
$tree->children[] = $folder;
}
else {
$parents = array();
$depth = $folder->get_namespace() == 'personal' ? 1 : 2;
while (count($path) >= $depth && ($parent = join($delim, $path))) {
array_pop($path);
$parent_parent = join($delim, $path);
if (!$refs[$parent]) {
if ($folder->type && self::folder_type($parent) == $folder->type) {
$refs[$parent] = new kolab_storage_folder($parent, $folder->type, $folder->type);
$refs[$parent]->parent = $parent_parent;
}
else if ($parent_parent == $other_ns) {
$refs[$parent] = new kolab_storage_folder_user($parent, $parent_parent);
}
else {
$name = kolab_storage::object_name($parent);
$refs[$parent] = new kolab_storage_folder_virtual($parent, $name, $folder->get_namespace(), $parent_parent);
}
$parents[] = $refs[$parent];
}
}
if (!empty($parents)) {
$parents = array_reverse($parents);
foreach ($parents as $parent) {
$parent_node = $refs[$parent->parent] ?: $tree;
$parent_node->children[] = $parent;
$_folders[] = $parent;
}
}
$parent_node = $refs[$folder->parent] ?: $tree;
$parent_node->children[] = $folder;
}
$refs[$folder->name] = $folder;
$_folders[] = $folder;
unset($folders[$idx]);
}
return $_folders;
}
/**
* Returns folder types indexed by folder name
*
* @param string $prefix Folder prefix (Default '*' for all folders)
*
* @return array|bool List of folders, False on failure
*/
public static function folders_typedata($prefix = '*')
{
if (!self::setup()) {
return false;
}
$type_keys = array(self::CTYPE_KEY, self::CTYPE_KEY_PRIVATE);
// fetch metadata from *some* folders only
if (($prefix == '*' || $prefix == '') && ($skip_ns = self::$config->get('kolab_skip_namespace'))) {
$delimiter = self::$imap->get_hierarchy_delimiter();
$folderdata = $blacklist = array();
foreach ((array)$skip_ns as $ns) {
if ($ns_root = rtrim(self::namespace_root($ns), $delimiter)) {
$blacklist[] = $ns_root;
}
}
foreach (array('personal','other','shared') as $ns) {
if (!in_array($ns, (array)$skip_ns)) {
$ns_root = rtrim(self::namespace_root($ns), $delimiter);
// list top-level folders and their childs one by one
// GETMETADATA "%" doesn't list shared or other namespace folders but "*" would
if ($ns_root == '') {
foreach ((array)self::$imap->get_metadata('%', $type_keys) as $folder => $metadata) {
if (!in_array($folder, $blacklist)) {
$folderdata[$folder] = $metadata;
$opts = self::$imap->folder_attributes($folder);
if (!in_array('\\HasNoChildren', $opts) && ($data = self::$imap->get_metadata($folder.$delimiter.'*', $type_keys))) {
$folderdata += $data;
}
}
}
}
else if ($data = self::$imap->get_metadata($ns_root.$delimiter.'*', $type_keys)) {
$folderdata += $data;
}
}
}
}
else {
$folderdata = self::$imap->get_metadata($prefix, $type_keys);
}
if (!is_array($folderdata)) {
return false;
}
return array_map(array('kolab_storage', 'folder_select_metadata'), $folderdata);
}
/**
* Callback for array_map to select the correct annotation value
*/
public static function folder_select_metadata($types)
{
if (!empty($types[self::CTYPE_KEY_PRIVATE])) {
return $types[self::CTYPE_KEY_PRIVATE];
}
else if (!empty($types[self::CTYPE_KEY])) {
list($ctype, ) = explode('.', $types[self::CTYPE_KEY]);
return $ctype;
}
return null;
}
/**
* Returns type of IMAP folder
*
* @param string $folder Folder name (UTF7-IMAP)
*
* @return string Folder type
*/
public static function folder_type($folder)
{
self::setup();
$metadata = self::$imap->get_metadata($folder, array(self::CTYPE_KEY, self::CTYPE_KEY_PRIVATE));
if (!is_array($metadata)) {
return null;
}
if (!empty($metadata[$folder])) {
return self::folder_select_metadata($metadata[$folder]);
}
return 'mail';
}
/**
* Sets folder content-type.
*
* @param string $folder Folder name
* @param string $type Content type
*
* @return boolean True on success
*/
public static function set_folder_type($folder, $type='mail')
{
self::setup();
list($ctype, $subtype) = explode('.', $type);
$success = self::$imap->set_metadata($folder, array(self::CTYPE_KEY => $ctype, self::CTYPE_KEY_PRIVATE => $subtype ? $type : null));
if (!$success) // fallback: only set private annotation
$success |= self::$imap->set_metadata($folder, array(self::CTYPE_KEY_PRIVATE => $type));
return $success;
}
/**
* Check subscription status of this folder
*
* @param string $folder Folder name
* @param boolean $temp Include temporary/session subscriptions
*
* @return boolean True if subscribed, false if not
*/
public static function folder_is_subscribed($folder, $temp = false)
{
if (self::$subscriptions === null) {
self::setup();
self::$with_tempsubs = false;
self::$subscriptions = self::$imap->list_folders_subscribed();
self::$with_tempsubs = true;
}
return in_array($folder, self::$subscriptions) ||
($temp && in_array($folder, (array)$_SESSION['kolab_subscribed_folders']));
}
/**
* Change subscription status of this folder
*
* @param string $folder Folder name
* @param boolean $temp Only subscribe temporarily for the current session
*
* @return True on success, false on error
*/
public static function folder_subscribe($folder, $temp = false)
{
self::setup();
// temporary/session subscription
if ($temp) {
if (self::folder_is_subscribed($folder)) {
return true;
}
else if (!is_array($_SESSION['kolab_subscribed_folders']) || !in_array($folder, $_SESSION['kolab_subscribed_folders'])) {
$_SESSION['kolab_subscribed_folders'][] = $folder;
return true;
}
}
else if (self::$imap->subscribe($folder)) {
self::$subscriptions = null;
return true;
}
return false;
}
/**
* Change subscription status of this folder
*
* @param string $folder Folder name
* @param boolean $temp Only remove temporary subscription
*
* @return True on success, false on error
*/
public static function folder_unsubscribe($folder, $temp = false)
{
self::setup();
// temporary/session subscription
if ($temp) {
if (is_array($_SESSION['kolab_subscribed_folders']) && ($i = array_search($folder, $_SESSION['kolab_subscribed_folders'])) !== false) {
unset($_SESSION['kolab_subscribed_folders'][$i]);
}
return true;
}
else if (self::$imap->unsubscribe($folder)) {
self::$subscriptions = null;
return true;
}
return false;
}
/**
* Check activation status of this folder
*
* @param string $folder Folder name
*
* @return boolean True if active, false if not
*/
public static function folder_is_active($folder)
{
$active_folders = self::get_states();
return in_array($folder, $active_folders);
}
/**
* Change activation status of this folder
*
* @param string $folder Folder name
*
* @return True on success, false on error
*/
public static function folder_activate($folder)
{
// activation implies temporary subscription
self::folder_subscribe($folder, true);
return self::set_state($folder, true);
}
/**
* Change activation status of this folder
*
* @param string $folder Folder name
*
* @return True on success, false on error
*/
public static function folder_deactivate($folder)
{
// remove from temp subscriptions, really?
self::folder_unsubscribe($folder, true);
return self::set_state($folder, false);
}
/**
* Return list of active folders
*/
private static function get_states()
{
if (self::$states !== null) {
return self::$states;
}
$rcube = rcube::get_instance();
$folders = $rcube->config->get('kolab_active_folders');
if ($folders !== null) {
self::$states = !empty($folders) ? explode('**', $folders) : array();
}
// for backward-compatibility copy server-side subscriptions to activation states
else {
self::setup();
if (self::$subscriptions === null) {
self::$with_tempsubs = false;
self::$subscriptions = self::$imap->list_folders_subscribed();
self::$with_tempsubs = true;
}
self::$states = (array) self::$subscriptions;
$folders = implode('**', self::$states);
$rcube->user->save_prefs(array('kolab_active_folders' => $folders));
}
return self::$states;
}
/**
* Update list of active folders
*/
private static function set_state($folder, $state)
{
self::get_states();
// update in-memory list
$idx = array_search($folder, self::$states);
if ($state && $idx === false) {
self::$states[] = $folder;
}
else if (!$state && $idx !== false) {
unset(self::$states[$idx]);
}
// update user preferences
$folders = implode('**', self::$states);
return rcube::get_instance()->user->save_prefs(array('kolab_active_folders' => $folders));
}
/**
* Creates default folder of specified type
* To be run when none of subscribed folders (of specified type) is found
*
* @param string $type Folder type
* @param string $props Folder properties (color, etc)
*
* @return string Folder name
*/
public static function create_default_folder($type, $props = array())
{
if (!self::setup()) {
return;
}
$folders = self::$imap->get_metadata('*', array(kolab_storage::CTYPE_KEY_PRIVATE));
// from kolab_folders config
$folder_type = strpos($type, '.') ? str_replace('.', '_', $type) : $type . '_default';
$default_name = self::$config->get('kolab_folders_' . $folder_type);
$folder_type = str_replace('_', '.', $folder_type);
// check if we have any folder in personal namespace
// folder(s) may exist but not subscribed
foreach ((array)$folders as $f => $data) {
if (strpos($data[self::CTYPE_KEY_PRIVATE], $type) === 0) {
$folder = $f;
break;
}
}
if (!$folder) {
if (!$default_name) {
$default_name = self::$default_folders[$type];
}
if (!$default_name) {
return;
}
$folder = rcube_charset::convert($default_name, RCUBE_CHARSET, 'UTF7-IMAP');
$prefix = self::$imap->get_namespace('prefix');
// add personal namespace prefix if needed
if ($prefix && strpos($folder, $prefix) !== 0 && $folder != 'INBOX') {
$folder = $prefix . $folder;
}
if (!self::$imap->folder_exists($folder)) {
if (!self::$imap->create_folder($folder)) {
return;
}
}
self::set_folder_type($folder, $folder_type);
}
self::folder_subscribe($folder);
if ($props['active']) {
self::set_state($folder, true);
}
if (!empty($props)) {
self::set_folder_props($folder, $props);
}
return $folder;
}
/**
* Sets folder metadata properties
*
* @param string $folder Folder name
* @param array &$prop Folder properties (color, displayname)
*/
public static function set_folder_props($folder, &$prop)
{
if (!self::setup()) {
return;
}
// TODO: also save 'showalarams' and other properties here
$ns = self::$imap->folder_namespace($folder);
$supported = array(
'color' => array(self::COLOR_KEY_SHARED, self::COLOR_KEY_PRIVATE),
'displayname' => array(self::NAME_KEY_SHARED, self::NAME_KEY_PRIVATE),
);
foreach ($supported as $key => $metakeys) {
if (array_key_exists($key, $prop)) {
$meta_saved = false;
if ($ns == 'personal') // save in shared namespace for personal folders
$meta_saved = self::$imap->set_metadata($folder, array($metakeys[0] => $prop[$key]));
if (!$meta_saved) // try in private namespace
$meta_saved = self::$imap->set_metadata($folder, array($metakeys[1] => $prop[$key]));
if ($meta_saved)
unset($prop[$key]); // unsetting will prevent fallback to local user prefs
}
}
}
/**
* Search users in Kolab LDAP storage
*
* @param mixed $query Search value (or array of field => value pairs)
* @param int $mode Matching mode: 0 - partial (*abc*), 1 - strict (=), 2 - prefix (abc*)
* @param array $required List of fields that shall ot be empty
* @param int $limit Maximum number of records
* @param int $count Returns the number of records found
*
* @return array List of users
*/
public static function search_users($query, $mode = 1, $required = array(), $limit = 0, &$count = 0)
{
$query = str_replace('*', '', $query);
// requires a working LDAP setup
if (!strlen($query) || !($ldap = self::ldap())) {
return array();
}
$root = self::namespace_root('other');
$user_attrib = self::$config->get('kolab_users_id_attrib', self::$config->get('kolab_auth_login', 'mail'));
$search_attrib = self::$config->get('kolab_users_search_attrib', array('cn','mail','alias'));
// search users using the configured attributes
$results = $ldap->dosearch($search_attrib, $query, $mode, $required, $limit, $count);
// exclude myself
if ($_SESSION['kolab_dn']) {
unset($results[$_SESSION['kolab_dn']]);
}
// resolve to IMAP folder name
array_walk($results, function(&$user, $dn) use ($root, $user_attrib) {
list($localpart, ) = explode('@', $user[$user_attrib]);
$user['kolabtargetfolder'] = $root . $localpart;
});
return $results;
}
/**
* Returns a list of IMAP folders shared by the given user
*
* @param array User entry from LDAP
* @param string Data type to list folders for (contact,event,task,journal,file,note,mail,configuration)
* @param int 1 - subscribed folders only, 0 - all folders, 2 - all non-active
* @param array Will be filled with folder-types data
*
* @return array List of folders
*/
public static function list_user_folders($user, $type, $subscribed = 0, &$folderdata = array())
{
self::setup();
$folders = array();
// use localpart of user attribute as root for folder listing
$user_attrib = self::$config->get('kolab_users_id_attrib', self::$config->get('kolab_auth_login', 'mail'));
if (!empty($user[$user_attrib])) {
list($mbox) = explode('@', $user[$user_attrib]);
$delimiter = self::$imap->get_hierarchy_delimiter();
$other_ns = self::namespace_root('other');
$prefix = $other_ns . $mbox . $delimiter;
$subscribed = (int) $subscribed;
$subs = $subscribed < 2 ? (bool) $subscribed : false;
$folders = self::list_folders($prefix, '*', $type, $subs, $folderdata);
if ($subscribed === 2 && !empty($folders)) {
$active = self::get_states();
if (!empty($active)) {
$folders = array_diff($folders, $active);
}
}
}
return $folders;
}
/**
* Get a list of (virtual) top-level folders from the other users namespace
*
* @param string Data type to list folders for (contact,event,task,journal,file,note,mail,configuration)
* @param boolean Enable to return subscribed folders only (null to use configured subscription mode)
*
* @return array List of kolab_storage_folder_user objects
*/
public static function get_user_folders($type, $subscribed)
{
$folders = $folderdata = array();
if (self::setup()) {
$delimiter = self::$imap->get_hierarchy_delimiter();
$other_ns = rtrim(self::namespace_root('other'), $delimiter);
$path_len = count(explode($delimiter, $other_ns));
foreach ((array)self::list_folders($other_ns . $delimiter, '*', '', $subscribed) as $foldername) {
if ($foldername == 'INBOX') // skip INBOX which is added by default
continue;
$path = explode($delimiter, $foldername);
// compare folder type if a subfolder is listed
if ($type && count($path) > $path_len + 1 && $type != self::folder_type($foldername)) {
continue;
}
// truncate folder path to top-level folders of the 'other' namespace
$foldername = join($delimiter, array_slice($path, 0, $path_len + 1));
if (!$folders[$foldername]) {
$folders[$foldername] = new kolab_storage_folder_user($foldername, $other_ns);
}
}
// for every (subscribed) user folder, list all (unsubscribed) subfolders
foreach ($folders as $userfolder) {
foreach ((array)self::list_folders($userfolder->name . $delimiter, '*', $type, false, $folderdata) as $foldername) {
if (!$folders[$foldername]) {
$folders[$foldername] = new kolab_storage_folder($foldername, $type, $folderdata[$foldername]);
$userfolder->children[] = $folders[$foldername];
}
}
}
}
return $folders;
}
/**
* Handler for user_delete plugin hooks
*
* Remove all cache data from the local database related to the given user.
*/
public static function delete_user_folders($args)
{
$db = rcmail::get_instance()->get_dbh();
$prefix = 'imap://' . urlencode($args['username']) . '@' . $args['host'] . '/%';
$db->query("DELETE FROM " . $db->table_name('kolab_folders', true) . " WHERE `resource` LIKE ?", $prefix);
}
/**
* Get folder METADATA for all supported keys
* Do this in one go for better caching performance
*/
public static function folder_metadata($folder)
{
if (self::setup()) {
$keys = array(
// For better performance we skip displayname here, see (self::custom_displayname())
// self::NAME_KEY_PRIVATE,
// self::NAME_KEY_SHARED,
self::CTYPE_KEY,
self::CTYPE_KEY_PRIVATE,
self::COLOR_KEY_PRIVATE,
self::COLOR_KEY_SHARED,
self::UID_KEY_SHARED,
self::UID_KEY_CYRUS,
);
$metadata = self::$imap->get_metadata($folder, $keys);
return $metadata[$folder];
}
}
/**
* Get user attributes for specified other user (imap) folder identifier.
*
* @param string $folder_id Folder name w/o path (imap user identifier)
* @param bool $as_string Return configured display name attribute value
*
* @return array User attributes
* @see self::ldap()
*/
public static function folder_id2user($folder_id, $as_string = false)
{
static $domain, $cache, $name_attr;
$rcube = rcube::get_instance();
if ($domain === null) {
list(, $domain) = explode('@', $rcube->get_user_name());
}
if ($name_attr === null) {
$name_attr = (array) ($rcube->config->get('kolab_users_name_field', $rcube->config->get('kolab_auth_name')) ?: 'name');
}
$token = $folder_id;
if ($domain && strpos($find, '@') === false) {
$token .= '@' . $domain;
}
if ($cache === null) {
$cache = $rcube->get_cache_shared('kolab_users') ?: false;
}
// use value cached in memory for repeated lookups
if (!$cache && array_key_exists($token, self::$ldapcache)) {
$user = self::$ldapcache[$token];
}
if (empty($user) && $cache) {
$user = $cache->get($token);
}
if (empty($user) && ($ldap = self::ldap())) {
$user = $ldap->get_user_record($token, $_SESSION['imap_host']);
if (!empty($user)) {
$keys = array('displayname', 'name', 'mail'); // supported keys
$user = array_intersect_key($user, array_flip($keys));
if (!empty($user)) {
if ($cache) {
$cache->set($token, $user);
}
else {
self::$ldapcache[$token] = $user;
}
}
}
}
if (!empty($user)) {
if ($as_string) {
foreach ($name_attr as $attr) {
if ($display = $user[$attr]) {
break;
}
}
if (!$display) {
$display = $user['displayname'] ?: $user['name'];
}
if ($display && $display != $folder_id) {
$display = "$display ($folder_id)";
}
return $display;
}
return $user;
}
}
/**
* Chwala's 'folder_mod' hook handler for mapping other users folder names
*/
public static function folder_mod($args)
{
static $roots;
if ($roots === null) {
self::setup();
$roots = self::$imap->get_namespace('other');
}
// Note: We're working with UTF7-IMAP encoding here
if ($args['dir'] == 'in') {
foreach ((array) $roots as $root) {
if (strpos($args['folder'], $root[0]) === 0) {
// remove root and explode folder
$delim = $root[1];
$folder = explode($delim, substr($args['folder'], strlen($root[0])));
// compare first (user) part with a regexp, it's supposed
// to look like this: "Doe, Jane (uid)", so we can extract the uid
// and replace the folder with it
if (preg_match('~^[^/]+ \(([^)]+)\)$~', $folder[0], $m)) {
$folder[0] = $m[1];
$args['folder'] = $root[0] . implode($delim, $folder);
}
break;
}
}
}
else { // dir == 'out'
foreach ((array) $roots as $root) {
if (strpos($args['folder'], $root[0]) === 0) {
// remove root and explode folder
$delim = $root[1];
$folder = explode($delim, substr($args['folder'], strlen($root[0])));
// Replace uid with "Doe, Jane (uid)"
if ($user = self::folder_id2user($folder[0], true)) {
$user = str_replace($delim, '', $user);
$folder[0] = rcube_charset::convert($user, RCUBE_CHARSET, 'UTF7-IMAP');
$args['folder'] = $root[0] . implode($delim, $folder);
}
break;
}
}
}
return $args;
}
}
diff --git a/plugins/libkolab/lib/kolab_storage_dav.php b/plugins/libkolab/lib/kolab_storage_dav.php
index f4480cf9..1d86d8d1 100644
--- a/plugins/libkolab/lib/kolab_storage_dav.php
+++ b/plugins/libkolab/lib/kolab_storage_dav.php
@@ -1,476 +1,482 @@
<?php
/**
* Kolab storage class providing access to groupware objects on a *DAV server.
*
* @author Aleksander Machniak <machniak@apheleia-it.ch>
*
* Copyright (C) 2022, Apheleia IT AG <contact@apheleia-it.ch>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_storage_dav
{
const ERROR_DAV_CONN = 1;
const ERROR_CACHE_DB = 2;
const ERROR_NO_PERMISSION = 3;
const ERROR_INVALID_FOLDER = 4;
protected $dav;
protected $url;
/**
* Object constructor
*/
public function __construct($url)
{
$this->url = $url;
$this->setup();
}
/**
* Setup the environment
*/
public function setup()
{
$rcmail = rcube::get_instance();
$this->config = $rcmail->config;
$this->dav = new kolab_dav_client($this->url);
}
/**
* Get a list of storage folders for the given data type
*
* @param string Data type to list folders for (contact,distribution-list,event,task,note)
*
* @return array List of kolab_storage_dav_folder objects
*/
public function get_folders($type)
{
+ $davTypes = [
+ 'event' => 'VEVENT',
+ 'task' => 'VTODO',
+ 'contact' => 'VCARD',
+ ];
+
// TODO: This should be cached
- $folders = $this->dav->discover();
+ $folders = $this->dav->discover($davTypes[$type]);
if (is_array($folders)) {
foreach ($folders as $idx => $folder) {
$folders[$idx] = new kolab_storage_dav_folder($this->dav, $folder, $type);
}
}
return $folders ?: [];
}
/**
* Getter for the storage folder for the given type
*
* @param string Data type to list folders for (contact,distribution-list,event,task,note)
*
* @return object kolab_storage_dav_folder The folder object
*/
public function get_default_folder($type)
{
// TODO: Not used
}
/**
* Getter for a specific storage folder
*
- * @param string Folder to access (UTF7-IMAP)
+ * @param string Folder to access
* @param string Expected folder type
*
* @return object kolab_storage_folder The folder object
*/
public function get_folder($folder, $type = null)
{
// TODO
}
/**
* Getter for a single Kolab object, identified by its UID.
* This will search all folders storing objects of the given type.
*
* @param string Object UID
* @param string Object type (contact,event,task,journal,file,note,configuration)
*
* @return array The Kolab object represented as hash array or false if not found
*/
public function get_object($uid, $type)
{
// TODO
return false;
}
/**
* Execute cross-folder searches with the given query.
*
* @param array Pseudo-SQL query as list of filter parameter triplets
* @param string Folder type (contact,event,task,journal,file,note,configuration)
* @param int Expected number of records or limit (for performance reasons)
*
* @return array List of Kolab data objects (each represented as hash array)
*/
public function select($query, $type, $limit = null)
{
$result = [];
foreach ($this->get_folders($type) as $folder) {
if ($limit) {
$folder->set_order_and_limit(null, $limit);
}
foreach ($folder->select($query) as $object) {
$result[] = $object;
}
}
return $result;
}
/**
* Compose an URL to query the free/busy status for the given user
*
* @param string Email address of the user to get free/busy data for
* @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
*/
public static function get_freebusy_url($email, $start = null, $end = null)
{
return kolab_storage::get_freebusy_url($email, $start, $end);
}
/**
* Deletes a folder
*
* @param string $name Folder name
*
* @return bool True on success, false on failure
*/
public function folder_delete($name)
{
// TODO
}
/**
* Creates a folder
*
* @param string $name Folder name (UTF7-IMAP)
* @param string $type Folder type
* @param bool $subscribed Sets folder subscription
* @param bool $active Sets folder state (client-side subscription)
*
* @return bool True on success, false on failure
*/
public function folder_create($name, $type = null, $subscribed = false, $active = false)
{
// TODO
}
/**
* Renames DAV folder
*
* @param string $oldname Old folder name (UTF7-IMAP)
* @param string $newname New folder name (UTF7-IMAP)
*
* @return bool True on success, false on failure
*/
public function folder_rename($oldname, $newname)
{
// TODO
}
/**
* Rename or Create a new folder.
*
* Does additional checks for permissions and folder name restrictions
*
* @param array &$prop Hash array with folder properties and metadata
* - name: Folder name
* - oldname: Old folder name when changed
* - parent: Parent folder to create the new one in
* - type: Folder type to create
* - subscribed: Subscribed flag (IMAP subscription)
* - active: Activation flag (client-side subscription)
*
* @return string|false New folder name or False on failure
*/
public function folder_update(&$prop)
{
// TODO
}
/**
* Getter for human-readable name of a folder
*
* @param string $folder Folder name (UTF7-IMAP)
* @param string $folder_ns Will be set to namespace name of the folder
*
* @return string Name of the folder-object
*/
public function object_name($folder, &$folder_ns = null)
{
// TODO: Shared folders
$folder_ns = 'personal';
return $folder;
}
/**
* Creates a SELECT field with folders list
*
* @param string $type Folder type
* @param array $attrs SELECT field attributes (e.g. name)
* @param string $current The name of current folder (to skip it)
*
* @return html_select SELECT object
*/
public function folder_selector($type, $attrs, $current = '')
{
// TODO
}
/**
* Returns a list of folder names
*
* @param string Optional root folder
* @param string Optional name pattern
* @param string Data type to list folders for (contact,event,task,journal,file,note,mail,configuration)
* @param bool Enable to return subscribed folders only (null to use configured subscription mode)
* @param array Will be filled with folder-types data
*
* @return array List of folders
*/
public function list_folders($root = '', $mbox = '*', $filter = null, $subscribed = null, &$folderdata = array())
{
// TODO
}
/**
* Search for shared or otherwise not listed groupware folders the user has access
*
* @param string Folder type of folders to search for
* @param string Search string
* @param array Namespace(s) to exclude results from
*
* @return array List of matching kolab_storage_folder objects
*/
public function search_folders($type, $query, $exclude_ns = [])
{
// TODO
return [];
}
/**
* Sort the given list of folders by namespace/name
*
* @param array List of kolab_storage_dav_folder objects
*
* @return array Sorted list of folders
*/
public static function sort_folders($folders)
{
// TODO
return $folders;
}
/**
* Returns folder types indexed by folder name
*
* @param string $prefix Folder prefix (Default '*' for all folders)
*
* @return array|bool List of folders, False on failure
*/
public function folders_typedata($prefix = '*')
{
// TODO: Used by kolab_folders, kolab_activesync, kolab_delegation
return [];
}
/**
* Returns type of a DAV folder
*
* @param string $folder Folder name (UTF7-IMAP)
*
* @return string Folder type
*/
public function folder_type($folder)
{
// TODO: Used by kolab_folders, kolab_activesync, kolab_delegation
return 'event';
}
/**
* Sets folder content-type.
*
* @param string $folder Folder name
* @param string $type Content type
*
* @return bool True on success, False otherwise
*/
public function set_folder_type($folder, $type = 'mail')
{
// NOP: Used by kolab_folders, kolab_activesync, kolab_delegation
return false;
}
/**
* Check subscription status of this folder
*
* @param string $folder Folder name
* @param bool $temp Include temporary/session subscriptions
*
* @return bool True if subscribed, false if not
*/
public function folder_is_subscribed($folder, $temp = false)
{
// NOP
return true;
}
/**
* Change subscription status of this folder
*
* @param string $folder Folder name
* @param bool $temp Only subscribe temporarily for the current session
*
* @return True on success, false on error
*/
public function folder_subscribe($folder, $temp = false)
{
// NOP
return true;
}
/**
* Change subscription status of this folder
*
* @param string $folder Folder name
* @param bool $temp Only remove temporary subscription
*
* @return True on success, false on error
*/
public function folder_unsubscribe($folder, $temp = false)
{
// NOP
return false;
}
/**
* Check activation status of this folder
*
* @param string $folder Folder name
*
* @return bool True if active, false if not
*/
public function folder_is_active($folder)
{
// TODO
return true;
}
/**
* Change activation status of this folder
*
* @param string $folder Folder name
*
* @return True on success, false on error
*/
public function folder_activate($folder)
{
return true;
}
/**
* Change activation status of this folder
*
* @param string $folder Folder name
*
* @return True on success, false on error
*/
public function folder_deactivate($folder)
{
return false;
}
/**
* Creates default folder of specified type
* To be run when none of subscribed folders (of specified type) is found
*
* @param string $type Folder type
* @param string $props Folder properties (color, etc)
*
* @return string Folder name
*/
public function create_default_folder($type, $props = [])
{
// TODO: For kolab_addressbook??
return '';
}
/**
* Returns a list of IMAP folders shared by the given user
*
* @param array User entry from LDAP
* @param string Data type to list folders for (contact,event,task,journal,file,note,mail,configuration)
* @param int 1 - subscribed folders only, 0 - all folders, 2 - all non-active
* @param array Will be filled with folder-types data
*
* @return array List of folders
*/
public function list_user_folders($user, $type, $subscribed = 0, &$folderdata = [])
{
// TODO
return [];
}
/**
* Get a list of (virtual) top-level folders from the other users namespace
*
* @param string Data type to list folders for (contact,event,task,journal,file,note,mail,configuration)
* @param bool Enable to return subscribed folders only (null to use configured subscription mode)
*
* @return array List of kolab_storage_folder_user objects
*/
public function get_user_folders($type, $subscribed)
{
// TODO
return [];
}
/**
* Handler for user_delete plugin hooks
*
* Remove all cache data from the local database related to the given user.
*/
public static function delete_user_folders($args)
{
$db = rcmail::get_instance()->get_dbh();
$table = $db->table_name('kolab_folders', true);
$prefix = 'dav://' . urlencode($args['username']) . '@' . $args['host'] . '/%';
$db->query("DELETE FROM $table WHERE `resource` LIKE ?", $prefix);
}
/**
* Get folder METADATA for all supported keys
* Do this in one go for better caching performance
*/
public function folder_metadata($folder)
{
// TODO ?
return [];
}
}
diff --git a/plugins/libkolab/lib/kolab_storage_dav_cache.php b/plugins/libkolab/lib/kolab_storage_dav_cache.php
index 7126516f..a6e3322b 100644
--- a/plugins/libkolab/lib/kolab_storage_dav_cache.php
+++ b/plugins/libkolab/lib/kolab_storage_dav_cache.php
@@ -1,622 +1,620 @@
<?php
/**
* Kolab storage cache class providing a local caching layer for Kolab groupware objects.
*
* @author Thomas Bruederli <bruederli@kolabsys.com>
* @author Aleksander Machniak <machniak@apheleia-it.ch>
*
* Copyright (C) 2012-2013, Kolab Systems AG <contact@kolabsys.com>
* Copyright (C) 2022, Apheleia IT AG <contact@apheleia-it.ch>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_storage_dav_cache extends kolab_storage_cache
{
/**
* Factory constructor
*/
public static function factory(kolab_storage_folder $storage_folder)
{
$subclass = 'kolab_storage_dav_cache_' . $storage_folder->type;
if (class_exists($subclass)) {
return new $subclass($storage_folder);
}
rcube::raise_error(
['code' => 900, 'message' => "No {$subclass} class found for folder '{$storage_folder->name}'"],
true
);
return new kolab_storage_dav_cache($storage_folder);
}
/**
* Connect cache with a storage folder
*
* @param kolab_storage_folder The storage folder instance to connect with
*/
public function set_folder(kolab_storage_folder $storage_folder)
{
$this->folder = $storage_folder;
if (!$this->folder->valid) {
$this->ready = false;
return;
}
// compose fully qualified ressource uri for this instance
$this->resource_uri = $this->folder->get_resource_uri();
$this->cache_table = $this->db->table_name('kolab_cache_dav_' . $this->folder->type);
$this->ready = true;
}
/**
* Synchronize local cache data with remote
*/
public function synchronize()
{
// only sync once per request cycle
if ($this->synched) {
return;
}
$this->sync_start = time();
// read cached folder metadata
$this->_read_folder_data();
$ctag = $this->folder->get_ctag();
// check cache status ($this->metadata is set in _read_folder_data())
if (
empty($this->metadata['ctag'])
|| empty($this->metadata['changed'])
|| $this->metadata['ctag'] !== $ctag
) {
// lock synchronization for this folder and wait if already locked
$this->_sync_lock();
$result = $this->synchronize_worker();
// update ctag value (will be written to database in _sync_unlock())
if ($result) {
$this->metadata['ctag'] = $ctag;
$this->metadata['changed'] = date(self::DB_DATE_FORMAT, time());
}
// remove lock
$this->_sync_unlock();
}
$this->synched = time();
}
/**
* Perform cache synchronization
*/
protected function synchronize_worker()
{
// get effective time limit we have for synchronization (~70% of the execution time)
$time_limit = $this->_max_sync_lock_time() * 0.7;
if (time() - $this->sync_start > $time_limit) {
return false;
}
// TODO: Implement synchronization with use of WebDAV-Sync (RFC 6578)
// Get the objects from the DAV server
$dav_index = $this->folder->dav->getIndex($this->folder->href, $this->folder->get_dav_type());
if (!is_array($dav_index)) {
rcube::raise_error([
'code' => 900,
'message' => "Failed to sync the kolab cache for {$this->folder->href}"
], true);
return false;
}
// WARNING: For now we assume object's href is <calendar-href>/<uid>.ics,
// which would mean there are no duplicates (objects with the same uid).
// With DAV protocol we can't get UID without fetching the whole object.
// Also the folder_id + uid is a unique index in the database.
// In the future we maybe should store the href in database.
// Determine objects to fetch or delete
$new_index = [];
$update_index = [];
$old_index = $this->current_index(); // uid -> etag
$chunk_size = 20; // max numer of objects per DAV request
foreach ($dav_index as $object) {
$uid = $object['uid'];
if (isset($old_index[$uid])) {
$old_etag = $old_index[$uid];
$old_index[$uid] = null;
if ($old_etag === $object['etag']) {
// the object didn't change
continue;
}
$update_index[$uid] = $object['href'];
}
else {
$new_index[$uid] = $object['href'];
}
}
// Fetch new objects and store in DB
if (!empty($new_index)) {
foreach (array_chunk($new_index, $chunk_size, true) as $chunk) {
$objects = $this->folder->dav->getData($this->folder->href, $chunk);
if (!is_array($objects)) {
rcube::raise_error([
'code' => 900,
'message' => "Failed to sync the kolab cache for {$this->folder->href}"
], true);
return false;
}
foreach ($objects as $object) {
if ($object = $this->folder->from_dav($object)) {
$this->_extended_insert(false, $object);
}
}
$this->_extended_insert(true, null);
// check time limit and abort sync if running too long
if (++$i % 25 == 0 && time() - $this->sync_start > $time_limit) {
return false;
}
}
}
// Fetch updated objects and store in DB
if (!empty($update_index)) {
foreach (array_chunk($update_index, $chunk_size, true) as $chunk) {
$objects = $this->folder->dav->getData($this->folder->href, $chunk);
if (!is_array($objects)) {
rcube::raise_error([
'code' => 900,
'message' => "Failed to sync the kolab cache for {$this->folder->href}"
], true);
return false;
}
foreach ($objects as $object) {
if ($object = $this->folder->from_dav($object)) {
$this->save($object, $object['uid']);
}
}
// check time limit and abort sync if running too long
if (++$i % 25 == 0 && time() - $this->sync_start > $time_limit) {
return false;
}
}
}
// Remove deleted objects
$old_index = array_filter($old_index);
if (!empty($old_index)) {
$quoted_uids = join(',', array_map(array($this->db, 'quote'), $old_index));
$this->db->query(
"DELETE FROM `{$this->cache_table}` WHERE `folder_id` = ? AND `uid` IN ($quoted_uids)",
$this->folder_id
);
}
return true;
}
/**
* Return current folder index (uid -> etag)
*/
protected function current_index()
{
// read cache index
$sql_result = $this->db->query(
- "SELECT `uid`, `data` FROM `{$this->cache_table}` WHERE `folder_id` = ?",
+ "SELECT `uid`, `etag` FROM `{$this->cache_table}` WHERE `folder_id` = ?",
$this->folder_id
);
$index = [];
- // TODO: Store etag as a separate column
-
while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
- if ($object = json_decode($sql_arr['data'], true)) {
- $index[$sql_arr['uid']] = $object['etag'];
- }
+ $index[$sql_arr['uid']] = $sql_arr['etag'];
}
return $index;
}
/**
* Read a single entry from cache or from server directly
*
* @param string Object UID
* @param string Object type to read
*/
public function get($uid, $type = null)
{
if ($this->ready) {
$this->_read_folder_data();
$sql_result = $this->db->query(
"SELECT * FROM `{$this->cache_table}` WHERE `folder_id` = ? AND `uid` = ?",
$this->folder_id,
$uid
);
if ($sql_arr = $this->db->fetch_assoc($sql_result)) {
$object = $this->_unserialize($sql_arr);
}
}
// fetch from DAV if not present in cache
if (empty($object)) {
if ($object = $this->folder->read_object($uid, $type ?: '*')) {
$this->save($object);
}
}
return $object ?: null;
}
/**
* Insert/Update a cache entry
*
* @param string Object UID
* @param array|false Hash array with object properties to save or false to delete the cache entry
*/
public function set($uid, $object)
{
// remove old entry
if ($this->ready) {
$this->_read_folder_data();
$this->db->query(
"DELETE FROM `{$this->cache_table}` WHERE `folder_id` = ? AND `uid` = ?",
$this->folder_id,
$uid
);
}
if ($object) {
$this->save($object);
}
}
/**
* Insert (or update) a cache entry
*
* @param mixed Hash array with object properties to save or false to delete the cache entry
* @param string Optional old message UID (for update)
*/
public function save($object, $olduid = null)
{
// write to cache
if ($this->ready) {
$this->_read_folder_data();
$sql_data = $this->_serialize($object);
$sql_data['folder_id'] = $this->folder_id;
$sql_data['uid'] = $object['uid'];
+ $sql_data['etag'] = $object['etag'];
$args = [];
- $cols = ['folder_id', 'uid', 'changed', 'data', 'tags', 'words'];
+ $cols = ['folder_id', 'uid', 'etag', 'changed', 'data', 'tags', 'words'];
$cols = array_merge($cols, $this->extra_cols);
foreach ($cols as $idx => $col) {
$cols[$idx] = $this->db->quote_identifier($col);
$args[] = $sql_data[$col];
}
if ($olduid) {
foreach ($cols as $idx => $col) {
$cols[$idx] = "$col = ?";
}
$query = "UPDATE `{$this->cache_table}` SET " . implode(', ', $cols)
. " WHERE `folder_id` = ? AND `uid` = ?";
$args[] = $this->folder_id;
$args[] = $olduid;
}
else {
$query = "INSERT INTO `{$this->cache_table}` (`created`, " . implode(', ', $cols)
. ") VALUES (" . $this->db->now() . str_repeat(', ?', count($cols)) . ")";
}
$result = $this->db->query($query, $args);
if (!$this->db->affected_rows($result)) {
rcube::raise_error([
'code' => 900,
'message' => "Failed to write to kolab cache"
], true);
}
}
}
/**
* Move an existing cache entry to a new resource
*
* @param string Entry's UID
* @param kolab_storage_folder Target storage folder instance
*/
public function move($uid, $target)
{
// TODO
}
/**
* Update resource URI for existing folder
*
* @param string Target DAV folder to move it to
*/
public function rename($new_folder)
{
// TODO
}
/**
* Select Kolab objects filtered by the given query
*
* @param array Pseudo-SQL query as list of filter parameter triplets
* triplet: ['<colname>', '<comparator>', '<value>']
* @param bool Set true to only return UIDs instead of complete objects
* @param bool Use fast mode to fetch only minimal set of information
* (no xml fetching and parsing, etc.)
*
* @return array|null|kolab_storage_dataset List of Kolab data objects (each represented as hash array) or UIDs
*/
public function select($query = [], $uids = false, $fast = false)
{
$result = $uids ? [] : new kolab_storage_dataset($this);
$this->_read_folder_data();
// fetch full object data on one query if a small result set is expected
$fetchall = !$uids && ($this->limit ? $this->limit[0] : ($count = $this->count($query))) < self::MAX_RECORDS;
// skip SELECT if we know it will return nothing
if ($count === 0) {
return $result;
}
$sql_query = "SELECT " . ($fetchall ? '*' : "`uid`")
. " FROM `{$this->cache_table}` WHERE `folder_id` = ?"
. $this->_sql_where($query)
. (!empty($this->order_by) ? " ORDER BY " . $this->order_by : '');
$sql_result = $this->limit ?
$this->db->limitquery($sql_query, $this->limit[1], $this->limit[0], $this->folder_id) :
$this->db->query($sql_query, $this->folder_id);
if ($this->db->is_error($sql_result)) {
if ($uids) {
return null;
}
$result->set_error(true);
return $result;
}
while ($sql_arr = $this->db->fetch_assoc($sql_result)) {
if ($fast) {
$sql_arr['fast-mode'] = true;
}
if ($uids) {
$result[] = $sql_arr['uid'];
}
else if ($fetchall && ($object = $this->_unserialize($sql_arr))) {
$result[] = $object;
}
else if (!$fetchall) {
$result[] = $sql_arr;
}
}
return $result;
}
/**
* Get number of objects mathing the given query
*
* @param array $query Pseudo-SQL query as list of filter parameter triplets
*
* @return int The number of objects of the given type
*/
public function count($query = [])
{
// read from local cache DB (assume it to be synchronized)
$this->_read_folder_data();
$sql_result = $this->db->query(
"SELECT COUNT(*) AS `numrows` FROM `{$this->cache_table}` ".
"WHERE `folder_id` = ?" . $this->_sql_where($query),
$this->folder_id
);
if ($this->db->is_error($sql_result)) {
return null;
}
$sql_arr = $this->db->fetch_assoc($sql_result);
$count = intval($sql_arr['numrows']);
return $count;
}
/**
* Getter for a single Kolab object identified by its UID
*
* @param string $uid Object UID
*
* @return array|null The Kolab object represented as hash array
*/
public function get_by_uid($uid)
{
$old_limit = $this->limit;
// set limit to skip count query
$this->limit = [1, 0];
$list = $this->select([['uid', '=', $uid]]);
// set the limit back to defined value
$this->limit = $old_limit;
if (!empty($list) && !empty($list[0])) {
return $list[0];
}
}
/**
* Check DAV connection error state
*/
protected function check_error()
{
// TODO ?
}
/**
* Write records into cache using extended inserts to reduce the number of queries to be executed
*
* @param bool Set to false to commit buffered insert, true to force an insert
* @param array Kolab object to cache
*/
protected function _extended_insert($force, $object)
{
static $buffer = '';
$line = '';
- $cols = ['folder_id', 'uid', 'created', 'changed', 'data', 'tags', 'words'];
+ $cols = ['folder_id', 'uid', 'etag', 'created', 'changed', 'data', 'tags', 'words'];
if ($this->extra_cols) {
$cols = array_merge($cols, $this->extra_cols);
}
if ($object) {
$sql_data = $this->_serialize($object);
// Skip multi-folder insert for all databases but MySQL
// In Oracle we can't put long data inline, others we don't support yet
if (strpos($this->db->db_provider, 'mysql') !== 0) {
$extra_args = [];
- $params = [$this->folder_id, $object['uid'], $sql_data['changed'],
+ $params = [$this->folder_id, $object['uid'], $object['etag'], $sql_data['changed'],
$sql_data['data'], $sql_data['tags'], $sql_data['words']];
foreach ($this->extra_cols as $col) {
$params[] = $sql_data[$col];
$extra_args[] = '?';
}
$cols = implode(', ', array_map(function($n) { return "`{$n}`"; }, $cols));
$extra_args = count($extra_args) ? ', ' . implode(', ', $extra_args) : '';
$result = $this->db->query(
"INSERT INTO `{$this->cache_table}` ($cols)"
. " VALUES (?, ?, " . $this->db->now() . ", ?, ?, ?, ?$extra_args)",
$params
);
if (!$this->db->affected_rows($result)) {
rcube::raise_error(array(
'code' => 900, 'message' => "Failed to write to kolab cache"
), true);
}
return;
}
$values = array(
$this->db->quote($this->folder_id),
$this->db->quote($object['uid']),
+ $this->db->quote($object['etag']),
$this->db->now(),
$this->db->quote($sql_data['changed']),
$this->db->quote($sql_data['data']),
$this->db->quote($sql_data['tags']),
$this->db->quote($sql_data['words']),
);
foreach ($this->extra_cols as $col) {
$values[] = $this->db->quote($sql_data[$col]);
}
$line = '(' . join(',', $values) . ')';
}
if ($buffer && ($force || (strlen($buffer) + strlen($line) > $this->max_sql_packet()))) {
$columns = implode(', ', array_map(function($n) { return "`{$n}`"; }, $cols));
$update = implode(', ', array_map(function($i) { return "`{$i}` = VALUES(`{$i}`)"; }, array_slice($cols, 2)));
$result = $this->db->query(
"INSERT INTO `{$this->cache_table}` ($columns) VALUES $buffer"
. " ON DUPLICATE KEY UPDATE $update"
);
if (!$this->db->affected_rows($result)) {
rcube::raise_error(array(
'code' => 900, 'message' => "Failed to write to kolab cache"
), true);
}
$buffer = '';
}
$buffer .= ($buffer ? ',' : '') . $line;
}
/**
* Helper method to turn stored cache data into a valid storage object
*/
protected function _unserialize($sql_arr)
{
if ($sql_arr['fast-mode'] && !empty($sql_arr['data']) && ($object = json_decode($sql_arr['data'], true))) {
- $object['uid'] = $sql_arr['uid'];
-
foreach ($this->data_props as $prop) {
if (isset($object[$prop]) && is_array($object[$prop]) && $object[$prop]['cl'] == 'DateTime') {
$object[$prop] = new DateTime($object[$prop]['dt'], new DateTimeZone($object[$prop]['tz']));
}
else if (!isset($object[$prop]) && isset($sql_arr[$prop])) {
$object[$prop] = $sql_arr[$prop];
}
}
if ($sql_arr['created'] && empty($object['created'])) {
$object['created'] = new DateTime($sql_arr['created']);
}
if ($sql_arr['changed'] && empty($object['changed'])) {
$object['changed'] = new DateTime($sql_arr['changed']);
}
$object['_type'] = $sql_arr['type'] ?: $this->folder->type;
+ $object['uid'] = $sql_arr['uid'];
+ $object['etag'] = $sql_arr['etag'];
}
// Fetch a complete object from the server
else {
// TODO: Fetching objects one-by-one from DAV server is slow
$object = $this->folder->read_object($sql_arr['uid'], '*');
}
return $object;
}
}
diff --git a/plugins/libkolab/lib/kolab_storage_dav_cache_contact.php b/plugins/libkolab/lib/kolab_storage_dav_cache_contact.php
new file mode 100644
index 00000000..66d2b830
--- /dev/null
+++ b/plugins/libkolab/lib/kolab_storage_dav_cache_contact.php
@@ -0,0 +1,116 @@
+<?php
+
+/**
+ * Kolab storage cache class for contact objects
+ *
+ * @author Aleksander Machniak <machniak@apcheleia-it.ch>
+ *
+ * Copyright (C) 2013-2022, Apheleia IT AG <contact@apcheleia-it.ch>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
+ */
+
+class kolab_storage_cache_contact extends kolab_storage_cache
+{
+ protected $extra_cols_max = 255;
+ protected $extra_cols = ['type', 'name', 'firstname', 'surname', 'email'];
+ protected $data_props = ['type', 'name', 'firstname', 'middlename', 'prefix', 'suffix', 'surname', 'email', 'organization', 'member'];
+ protected $fulltext_cols = ['name', 'firstname', 'surname', 'middlename', 'email:address'];
+
+ /**
+ * Helper method to convert the given Kolab object into a dataset to be written to cache
+ *
+ * @override
+ */
+ protected function _serialize($object)
+ {
+ $sql_data = parent::_serialize($object);
+ $sql_data['type'] = $object['_type'];
+
+ // columns for sorting
+ $sql_data['name'] = rcube_charset::clean($object['name'] . $object['prefix']);
+ $sql_data['firstname'] = rcube_charset::clean($object['firstname'] . $object['middlename'] . $object['surname']);
+ $sql_data['surname'] = rcube_charset::clean($object['surname'] . $object['firstname'] . $object['middlename']);
+ $sql_data['email'] = rcube_charset::clean(is_array($object['email']) ? $object['email'][0] : $object['email']);
+
+ if (is_array($sql_data['email'])) {
+ $sql_data['email'] = $sql_data['email']['address'];
+ }
+ // avoid value being null
+ if (empty($sql_data['email'])) {
+ $sql_data['email'] = '';
+ }
+
+ // use organization if name is empty
+ if (empty($sql_data['name']) && !empty($object['organization'])) {
+ $sql_data['name'] = rcube_charset::clean($object['organization']);
+ }
+
+ // make sure some data is not longer that database limit (#5291)
+ foreach ($this->extra_cols as $col) {
+ if (strlen($sql_data[$col]) > $this->extra_cols_max) {
+ $sql_data[$col] = rcube_charset::clean(substr($sql_data[$col], 0, $this->extra_cols_max));
+ }
+ }
+
+ $sql_data['tags'] = ' ' . join(' ', $this->get_tags($object)) . ' '; // pad with spaces for strict/prefix search
+ $sql_data['words'] = ' ' . join(' ', $this->get_words($object)) . ' ';
+
+ return $sql_data;
+ }
+
+ /**
+ * Callback to get words to index for fulltext search
+ *
+ * @return array List of words to save in cache
+ */
+ public function get_words($object)
+ {
+ $data = '';
+ foreach ($this->fulltext_cols as $colname) {
+ list($col, $field) = explode(':', $colname);
+
+ if ($field) {
+ $a = [];
+ foreach ((array)$object[$col] as $attr)
+ $a[] = $attr[$field];
+ $val = join(' ', $a);
+ }
+ else {
+ $val = is_array($object[$col]) ? join(' ', $object[$col]) : $object[$col];
+ }
+
+ if (strlen($val))
+ $data .= $val . ' ';
+ }
+
+ return array_unique(rcube_utils::normalize_string($data, true));
+ }
+
+ /**
+ * Callback to get object specific tags to cache
+ *
+ * @return array List of tags to save in cache
+ */
+ public function get_tags($object)
+ {
+ $tags = [];
+
+ if (!empty($object['birthday'])) {
+ $tags[] = 'x-has-birthday';
+ }
+
+ return $tags;
+ }
+}
diff --git a/plugins/libkolab/lib/kolab_storage_dav_cache_event.php b/plugins/libkolab/lib/kolab_storage_dav_cache_event.php
index 91a57952..b6c3a16e 100644
--- a/plugins/libkolab/lib/kolab_storage_dav_cache_event.php
+++ b/plugins/libkolab/lib/kolab_storage_dav_cache_event.php
@@ -1,68 +1,146 @@
<?php
/**
* Kolab storage cache class for calendar event objects
*
* @author Thomas Bruederli <bruederli@kolabsys.com>
*
* Copyright (C) 2013, Kolab Systems AG <contact@kolabsys.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_storage_dav_cache_event extends kolab_storage_dav_cache
{
- protected $extra_cols = array('dtstart','dtend');
- protected $data_props = array('categories', 'status', 'attendees', 'etag');
+ protected $extra_cols = ['dtstart','dtend'];
+ protected $data_props = ['categories', 'status', 'attendees'];
+ protected $fulltext_cols = ['title', 'description', 'location', 'attendees:name', 'attendees:email', 'categories'];
/**
* Helper method to convert the given Kolab object into a dataset to be written to cache
*
* @override
*/
protected function _serialize($object)
{
$sql_data = parent::_serialize($object);
$sql_data['dtstart'] = $this->_convert_datetime($object['start']);
$sql_data['dtend'] = $this->_convert_datetime($object['end']);
// extend date range for recurring events
if (!empty($object['recurrence']) && !empty($object['_formatobj'])) {
$recurrence = new kolab_date_recurrence($object['_formatobj']);
$dtend = $recurrence->end() ?: new DateTime('now +100 years');
$sql_data['dtend'] = $this->_convert_datetime($dtend);
}
// extend start/end dates to spawn all exceptions
if (is_array($object['exceptions'])) {
foreach ($object['exceptions'] as $exception) {
if (is_a($exception['start'], 'DateTime')) {
$exstart = $this->_convert_datetime($exception['start']);
if ($exstart < $sql_data['dtstart']) {
$sql_data['dtstart'] = $exstart;
}
}
if (is_a($exception['end'], 'DateTime')) {
$exend = $this->_convert_datetime($exception['end']);
if ($exend > $sql_data['dtend']) {
$sql_data['dtend'] = $exend;
}
}
}
}
+ $sql_data['tags'] = ' ' . join(' ', $this->get_tags($object)) . ' '; // pad with spaces for strict/prefix search
+ $sql_data['words'] = ' ' . join(' ', $this->get_words($object)) . ' ';
+
return $sql_data;
}
+
+ /**
+ * Callback to get words to index for fulltext search
+ *
+ * @return array List of words to save in cache
+ */
+ public function get_words($object = [])
+ {
+ $data = '';
+
+ foreach ($this->fulltext_cols as $colname) {
+ list($col, $field) = explode(':', $colname);
+
+ if ($field) {
+ $a = [];
+ foreach ((array) $object[$col] as $attr) {
+ $a[] = $attr[$field];
+ }
+ $val = join(' ', $a);
+ }
+ else {
+ $val = is_array($object[$col]) ? join(' ', $object[$col]) : $object[$col];
+ }
+
+ if (strlen($val))
+ $data .= $val . ' ';
+ }
+
+ $words = rcube_utils::normalize_string($data, true);
+
+ // collect words from recurrence exceptions
+ if (is_array($object['exceptions'])) {
+ foreach ($object['exceptions'] as $exception) {
+ $words = array_merge($words, $this->get_words($exception));
+ }
+ }
+
+ return array_unique($words);
+ }
+
+ /**
+ * Callback to get object specific tags to cache
+ *
+ * @return array List of tags to save in cache
+ */
+ public function get_tags($object)
+ {
+ $tags = [];
+
+ if (!empty($object['valarms'])) {
+ $tags[] = 'x-has-alarms';
+ }
+
+ // create tags reflecting participant status
+ if (is_array($object['attendees'])) {
+ foreach ($object['attendees'] as $attendee) {
+ if (!empty($attendee['email']) && !empty($attendee['status']))
+ $tags[] = 'x-partstat:' . $attendee['email'] . ':' . strtolower($attendee['status']);
+ }
+ }
+
+ // collect tags from recurrence exceptions
+ if (is_array($object['exceptions'])) {
+ foreach ($object['exceptions'] as $exception) {
+ $tags = array_merge($tags, $this->get_tags($exception));
+ }
+ }
+
+ if (!empty($object['status'])) {
+ $tags[] = 'x-status:' . strtolower($object['status']);
+ }
+
+ return array_unique($tags);
+ }
}
diff --git a/plugins/libkolab/lib/kolab_storage_dav_folder.php b/plugins/libkolab/lib/kolab_storage_dav_folder.php
index 7bee428a..5a8cdbf7 100644
--- a/plugins/libkolab/lib/kolab_storage_dav_folder.php
+++ b/plugins/libkolab/lib/kolab_storage_dav_folder.php
@@ -1,529 +1,541 @@
<?php
/**
* A class representing a DAV folder object.
*
* @author Aleksander Machniak <machniak@apheleia-it.ch>
*
* Copyright (C) 2014-2022, Apheleia IT AG <contact@apheleia-it.ch>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class kolab_storage_dav_folder extends kolab_storage_folder
{
public $dav;
public $href;
public $attributes;
/**
* Object constructor
*/
public function __construct($dav, $attributes, $type_annotation = '')
{
$this->attributes = $attributes;
$this->href = $this->attributes['href'];
// Here we assume the last element of the folder path is the folder ID
// if that's not the case, we should consider generating an ID
$href = explode('/', unslashify($this->href));
$this->id = $href[count($href) - 1];
$this->dav = $dav;
$this->valid = true;
list($this->type, $suffix) = explode('.', $type_annotation);
$this->default = $suffix == 'default';
$this->subtype = $this->default ? '' : $suffix;
// Init cache
$this->cache = kolab_storage_dav_cache::factory($this);
}
/**
* Returns the owner of the folder.
*
* @param bool Return a fully qualified owner name (i.e. including domain for shared folders)
*
* @return string The owner of this folder.
*/
public function get_owner($fully_qualified = false)
{
// return cached value
if (isset($this->owner)) {
return $this->owner;
}
$rcube = rcube::get_instance();
$this->owner = $rcube->get_user_name();
$this->valid = true;
// TODO: Support shared folders
return $this->owner;
}
/**
* Get a folder Etag identifier
*/
public function get_ctag()
{
return $this->attributes['ctag'];
}
/**
* Getter for the name of the namespace to which the folder belongs
*
* @return string Name of the namespace (personal, other, shared)
*/
public function get_namespace()
{
// TODO: Support shared folders
return 'personal';
}
/**
* Get the display name value of this folder
*
* @return string Folder name
*/
public function get_name()
{
return kolab_storage_dav::object_name($this->attributes['name']);
}
/**
* Getter for the top-end folder name (not the entire path)
*
* @return string Name of this folder
*/
public function get_foldername()
{
return $this->attributes['name'];
}
+ public function get_folder_info()
+ {
+ return []; // todo ?
+ }
+
/**
* Getter for parent folder path
*
* @return string Full path to parent folder
*/
public function get_parent()
{
// TODO
return '';
}
/**
* Compose a unique resource URI for this folder
*/
public function get_resource_uri()
{
if (!empty($this->resource_uri)) {
return $this->resource_uri;
}
// compose fully qualified ressource uri for this instance
$host = preg_replace('|^https?://|', 'dav://' . urlencode($this->get_owner(true)) . '@', $this->dav->url);
$path = $this->href[0] == '/' ? $this->href : "/{$this->href}";
$this->resource_uri = unslashify($host) . $path;
return $this->resource_uri;
}
/**
* Getter for the Cyrus mailbox identifier corresponding to this folder
* (e.g. user/john.doe/Calendar/Personal@example.org)
*
* @return string Mailbox ID
*/
public function get_mailbox_id()
{
// TODO: This is used with Bonnie related features
return '';
}
/**
* Get the color value stored in metadata
*
* @param string Default color value to return if not set
*
* @return mixed Color value from the folder metadata or $default if not set
*/
public function get_color($default = null)
{
return !empty($this->attributes['color']) ? $this->attributes['color'] : $default;
}
/**
* Get ACL information for this folder
*
* @return string Permissions as string
*/
public function get_myrights()
{
// TODO
return '';
}
/**
* Helper method to extract folder UID
*
* @return string Folder's UID
*/
public function get_uid()
{
// TODO ???
return '';
}
/**
* Check activation status of this folder
*
* @return bool True if enabled, false if not
*/
public function is_active()
{
// TODO
return true;
}
/**
* Change activation status of this folder
*
* @param bool The desired subscription status: true = active, false = not active
*
* @return bool True on success, false on error
*/
public function activate($active)
{
// TODO
return true;
}
/**
* Check subscription status of this folder
*
* @return bool True if subscribed, false if not
*/
public function is_subscribed()
{
// TODO
return true;
}
/**
* Change subscription status of this folder
*
* @param bool The desired subscription status: true = subscribed, false = not subscribed
*
* @return True on success, false on error
*/
public function subscribe($subscribed)
{
// TODO
return true;
}
/**
* Delete the specified object from this folder.
*
* @param array|string $object The Kolab object to delete or object UID
* @param bool $expunge Should the folder be expunged?
*
* @return bool True if successful, false on error
*/
public function delete($object, $expunge = true)
{
if (!$this->valid) {
return false;
}
$uid = is_array($object) ? $object['uid'] : $object;
$success = $this->dav->delete($this->object_location($uid), $content);
if ($success) {
$this->cache->set($uid, false);
}
return $success;
}
/**
*
*/
public function delete_all()
{
if (!$this->valid) {
return false;
}
// TODO: This method is used by kolab_addressbook plugin only
$this->cache->purge();
return false;
}
/**
* Restore a previously deleted object
*
* @param string $uid Object UID
*
* @return mixed Message UID on success, false on error
*/
public function undelete($uid)
{
if (!$this->valid) {
return false;
}
// TODO
return false;
}
/**
* Move a Kolab object message to another IMAP folder
*
* @param string Object UID
* @param string IMAP folder to move object to
*
* @return bool True on success, false on failure
*/
public function move($uid, $target_folder)
{
if (!$this->valid) {
return false;
}
// TODO
return false;
}
/**
* Save an object in this folder.
*
* @param array $object The array that holds the data of the object.
* @param string $type The type of the kolab object.
* @param string $uid The UID of the old object if it existed before
*
* @return mixed False on error or object UID on success
*/
public function save(&$object, $type = null, $uid = null)
{
if (!$this->valid || empty($object)) {
return false;
}
if (!$type) {
$type = $this->type;
}
/*
// copy attachments from old message
$copyfrom = $object['_copyfrom'] ?: $object['_msguid'];
if (!empty($copyfrom) && ($old = $this->cache->get($copyfrom, $type, $object['_mailbox']))) {
foreach ((array)$old['_attachments'] as $key => $att) {
if (!isset($object['_attachments'][$key])) {
$object['_attachments'][$key] = $old['_attachments'][$key];
}
// unset deleted attachment entries
if ($object['_attachments'][$key] == false) {
unset($object['_attachments'][$key]);
}
// load photo.attachment from old Kolab2 format to be directly embedded in xcard block
else if ($type == 'contact' && ($key == 'photo.attachment' || $key == 'kolab-picture.png') && $att['id']) {
if (!isset($object['photo']))
$object['photo'] = $this->get_attachment($copyfrom, $att['id'], $object['_mailbox']);
unset($object['_attachments'][$key]);
}
}
}
// process attachments
if (is_array($object['_attachments'])) {
$numatt = count($object['_attachments']);
foreach ($object['_attachments'] as $key => $attachment) {
// FIXME: kolab_storage and Roundcube attachment hooks use different fields!
if (empty($attachment['content']) && !empty($attachment['data'])) {
$attachment['content'] = $attachment['data'];
unset($attachment['data'], $object['_attachments'][$key]['data']);
}
// make sure size is set, so object saved in cache contains this info
if (!isset($attachment['size'])) {
if (!empty($attachment['content'])) {
if (is_resource($attachment['content'])) {
// this need to be a seekable resource, otherwise
// fstat() failes and we're unable to determine size
// here nor in rcube_imap_generic before IMAP APPEND
$stat = fstat($attachment['content']);
$attachment['size'] = $stat ? $stat['size'] : 0;
}
else {
$attachment['size'] = strlen($attachment['content']);
}
}
else if (!empty($attachment['path'])) {
$attachment['size'] = filesize($attachment['path']);
}
$object['_attachments'][$key] = $attachment;
}
// generate unique keys (used as content-id) for attachments
if (is_numeric($key) && $key < $numatt) {
// derrive content-id from attachment file name
$ext = preg_match('/(\.[a-z0-9]{1,6})$/i', $attachment['name'], $m) ? $m[1] : null;
$basename = preg_replace('/[^a-z0-9_.-]/i', '', basename($attachment['name'], $ext)); // to 7bit ascii
if (!$basename) $basename = 'noname';
$cid = $basename . '.' . microtime(true) . $key . $ext;
$object['_attachments'][$cid] = $attachment;
unset($object['_attachments'][$key]);
}
}
}
*/
$rcmail = rcube::get_instance();
$result = false;
// generate and save object message
if ($content = $this->to_dav($object)) {
- $result = $this->dav->create($this->object_location($object['uid']), $content);
+ $method = $uid ? 'update' : 'create';
+ $result = $this->dav->{$method}($this->object_location($object['uid']), $content);
+ // Note: $result can be NULL if the request was successful, but ETag wasn't returned
if ($result !== false) {
// insert/update object in the cache
$object['etag'] = $result;
$this->cache->save($object, $uid);
+ $result = true;
}
}
return $result;
}
/**
* Fetch the object the DAV server and convert to internal format
*
* @param string The object UID to fetch
* @param string The object type expected (use wildcard '*' to accept all types)
*
* @return mixed Hash array representing the Kolab object, a kolab_format instance or false if not found
*/
public function read_object($uid, $type = null)
{
if (!$this->valid) {
return false;
}
$href = $this->object_location($uid);
- $objects = $this->dav->getData($this->href, [$href]);
+ $objects = $this->dav->getData($this->href, $this->get_dav_type(), [$href]);
if (!is_array($objects) || count($objects) != 1) {
rcube::raise_error([
'code' => 900,
'message' => "Failed to fetch {$href}"
], true);
return false;
}
return $this->from_dav($objects[0]);
}
/**
* Convert DAV object into PHP array
*
* @param array Object data in kolab_dav_client::fetchData() format
*
* @return array Object properties
*/
public function from_dav($object)
{
if ($this->type == 'event') {
$ical = libcalendaring::get_ical();
$events = $ical->import($object['data']);
if (!count($events) || empty($events[0]['uid'])) {
return false;
}
$result = $events[0];
}
$result['etag'] = $object['etag'];
$result['href'] = $object['href'];
$result['uid'] = $object['uid'] ?: $result['uid'];
return $result;
}
/**
* Convert Kolab object into DAV format (iCalendar)
*/
public function to_dav($object)
{
$result = '';
if ($this->type == 'event') {
$ical = libcalendaring::get_ical();
- // TODO: Attachments?
+
+ if (!empty($object['exceptions'])) {
+ $object['recurrence']['EXCEPTIONS'] = $object['exceptions'];
+ }
+
$result = $ical->export([$object]);
}
return $result;
}
protected function object_location($uid)
{
return unslashify($this->href) . '/' . urlencode($uid) . '.' . $this->get_dav_ext();
}
/**
* Get a folder DAV content type
*/
public function get_dav_type()
{
$types = [
'event' => 'VEVENT',
'task' => 'VTODO',
'contact' => 'VCARD',
];
return $types[$this->type];
}
/**
* Get a DAV file extension for specified Kolab type
*/
public function get_dav_ext()
{
$types = [
'event' => 'ics',
'task' => 'ics',
'contact' => 'vcf',
];
return $types[$this->type];
}
/**
* Return folder name as string representation of this object
*
* @return string Full IMAP folder name
*/
public function __toString()
{
return $this->attributes['name'];
}
}
File Metadata
Details
Attached
Mime Type
text/x-diff
Expires
Sat, Mar 1, 3:36 AM (1 d, 4 h)
Storage Engine
blob
Storage Format
Raw Data
Storage Handle
165661
Default Alt Text
(481 KB)
Attached To
Mode
R14 roundcubemail-plugins-kolab
Attached
Detach File
Event Timeline
Log In to Comment