Added (modified) copy of Roundcube calendar from https://github.com/rc-calendar/calendar

This commit is contained in:
Thomas Bruederli 2011-05-20 19:04:25 +02:00
parent f56141bdcc
commit be8a0e0a7a
42 changed files with 10657 additions and 0 deletions

7
plugins/calendar/.gitignore vendored Normal file
View file

@ -0,0 +1,7 @@
*.swp
*.bak
*.old
*~
config.inc.php
skins/*
!skins/default

37
plugins/calendar/TODO Normal file
View file

@ -0,0 +1,37 @@
+ Edit: 3.12: Subject
+ Edit: 3.13: Location
+ Edit: 3.14: Start / End / All Day
+ Edit: 3.15: Show time as: Busy, Free, Out of office
- Edit: 3.16: Reminder set
+ Edit: 3.17: Priority: High/Low
- Edit: 3.19: Attachment Upload
- Edit: 3.20: Print
- Recurring events
- Add/Manage Attendees
- Edit: 3.21: Required / Optional / Resource specification
- Edit: 3.22: Conflict Handling (Free/Busy Check for attendees)
- Edit: 3.23: Specify folder for new event (prefs)
- View: 3.1: Folder list
- View: 3.3: Display modes (agenda / day / week / month)
+ Day / Week / Month
- List (Agenda) view
- Individual days selection
- View: 3.4: Fish-Eye View For Busy Days
- View: 3.5: Search
+ Show list of calendars in a (hideable) drawer
+ View: 3.6: Combined calendar view (Turn calendars on/off)
+ View: 3.7: Small month overview calendar
- View: 3.8: Color according to calendar and category (similar to Kontact)
+ View: 3.9: Alter event with drag/drop
- Option: 4.12: Set default reminder time
+ Support for multiple calendars (replace categories)
- Remember last visited view
- Allow user to create/edit/delete calendars
- Colors for calendars should be user-configurable
- ICS parser/generator ((http://code.google.com/p/qcal/)
- Importing ICS files
- Create/manage invdividual views
- Support for tasks/todos with task list view (ordered by date/time)
? Show and edit two different views
? Use ICS format for storage in DB

View file

@ -0,0 +1,768 @@
/*
+-------------------------------------------------------------------------+
| Javascript for the Calendar Plugin |
| Version 0.3 beta |
| |
| This program is free software; you can redistribute it and/or modify |
| it under the terms of the GNU General Public License version 2 |
| as published by the Free Software Foundation. |
| |
| 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 General Public License for more details. |
| |
| You should have received a copy of the GNU General Public License along |
| with this program; if not, write to the Free Software Foundation, Inc., |
| 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
| |
+-------------------------------------------------------------------------+
| Author: Lazlo Westerhof <hello@lazlo.me> |
| Thomas Bruederli <roundcube@gmail.com> |
+-------------------------------------------------------------------------+
*/
/* calendar initialization */
window.rcmail && rcmail.addEventListener('init', function(evt) {
// quote html entities
function Q(str)
{
return String(str).replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
}
// php equivalent
function nl2br(str)
{
return String(str).replace(/\n/g, "<br/>");
}
// Roundcube calendar client class
function rcube_calendar(settings)
{
this.settings = settings;
var me = this;
// private vars
var day_clicked = 0;
var ignore_click = false;
// data loader
var load_events = function(calendar, start, end, callback) {
$.ajax({
url: "./?_task=calendar&_action=plugin.load_events",
dataType: 'json',
data: {
source: calendar,
start: Math.round(start.getTime() / 1000),
end: Math.round(end.getTime() / 1000)
},
success: callback
});
};
// event details dialog (show only)
var event_show_dialog = function(event) {
var $dialog = $("#eventshow");
var calendar = event.calendar && me.calendars[event.calendar] ? me.calendars[event.calendar] : { editable:false };
$dialog.find('div.event-section, div.event-line').hide();
$('#event-title').html(Q(event.title)).show();
if (event.location)
$('#event-location').html('@ ' + Q(event.location)).show();
if (event.description)
$('#event-description').show().children('.event-text').html(nl2br(Q(event.description))); // TODO: format HTML with clickable links and stuff
// TODO: create a nice human-readable string for the date/time range
var fromto, duration = event.end.getTime() / 1000 - event.start.getTime() / 1000;
if (event.allDay)
fromto = $.fullCalendar.formatDate(event.start, settings['date_format']) + ' &mdash; ' + $.fullCalendar.formatDate(event.end, settings['date_format']);
else if (duration < 86400 && event.start.getDay() == event.end.getDay())
fromto = $.fullCalendar.formatDate(event.start, settings['date_format']) + ' ' + $.fullCalendar.formatDate(event.start, settings['time_format']) + ' &mdash; '
+ $.fullCalendar.formatDate(event.end, settings['time_format']);
else
fromto = $.fullCalendar.formatDate(event.start, settings['date_format']) + ' ' + $.fullCalendar.formatDate(event.start, settings['time_format']) + ' &mdash; '
+ $.fullCalendar.formatDate(event.end, settings['date_format']) + ' ' + $.fullCalendar.formatDate(event.end, settings['time_format']);
$('#event-date').html(Q(fromto)).show();
if (event.recurrence && event.recurrence_text)
$('#event-repeat').show().children('.event-text').html(Q(event.recurrence_text));
if (event.reminders && event.reminders_text)
$('#event-alarm').show().children('.event-text').html(Q(event.reminders_text));
if (calendar.name)
$('#event-calendar').show().children('.event-text').html(Q(calendar.name)).removeClass().addClass('event-text').addClass('cal-'+calendar.id);
if (event.categories)
$('#event-category').show().children('.event-text').html(Q(event.categories)).removeClass().addClass('event-text '+event.className);
if (event.free_busy)
$('#event-free-busy').show().children('.event-text').html(Q(rcmail.gettext(event.free_busy, 'calendar')));
if (event.priority != 1) {
var priolabels = { 0:rcmail.gettext('low'), 1:rcmail.gettext('normal'), 2:rcmail.gettext('high') };
$('#event-priority').show().children('.event-text').html(Q(priolabels[event.priority]));
}
var buttons = {};
if (calendar.editable) {
buttons[rcmail.gettext('edit', 'calendar')] = function() {
event_edit_dialog('edit', event);
};
buttons[rcmail.gettext('remove', 'calendar')] = function() {
me.delete_event(event);
$dialog.dialog('close');
};
}
else {
buttons[rcmail.gettext('close', 'calendar')] = function(){
$dialog.dialog('close');
};
}
// open jquery UI dialog
$dialog.dialog({
modal: false,
resizable: true,
title: null,
close: function() {
$dialog.dialog('destroy');
$dialog.hide();
},
buttons: buttons,
minWidth: 320,
width: 420
}).show();
$('<a>')
.attr('href', '#')
.html('More Options')
.addClass('dropdown-link')
.click(function(){ return false; })
.insertBefore($dialog.parent().find('.ui-dialog-buttonset').children().first());
};
// bring up the event dialog (jquery-ui popup)
var event_edit_dialog = function(action, event) {
// close show dialog first
$("#eventshow").dialog('close');
var $dialog = $("#eventedit");
var calendar = event.calendar && me.calendars[event.calendar] ? me.calendars[event.calendar] : { editable:action=='new' };
// reset dialog first, enable/disable fields according to editable state
$('#eventtabs').get(0).reset();
$('#calendar-select')[(action == 'new' ? 'show' : 'hide')]();
// event details
var title = $('#edit-title').val(event.title);
var location = $('#edit-location').val(event.location);
var description = $('#edit-description').val(event.description);
var categories = $('#edit-categories').val(event.categories);
var calendars = $('#edit-calendar').val(event.calendar);
var freebusy = $('#edit-free-busy').val(event.free_busy);
var priority = $('#edit-priority').val(event.priority);
var duration = Math.round((event.end.getTime() - event.start.getTime()) / 1000);
var startdate = $('#edit-startdate').val($.fullCalendar.formatDate(event.start, settings['date_format'])).data('duration', duration);
var starttime = $('#edit-starttime').val($.fullCalendar.formatDate(event.start, settings['time_format'])).show();
var enddate = $('#edit-enddate').val($.fullCalendar.formatDate(event.end, settings['date_format']));
var endtime = $('#edit-endtime').val($.fullCalendar.formatDate(event.end, settings['time_format'])).show();
var allday = $('#edit-allday').get(0);
if (event.allDay) {
starttime.val("00:00").hide();
endtime.val("23:59").hide();
allday.checked = true;
}
else {
allday.checked = false;
}
// set alarm(s)
// TODO: support multiple alarm entries
if (event.alarms) {
if (typeof event.alarms == 'string')
event.alarms = event.alarms.split(';');
for (var alarm, i=0; i < event.alarms.length; i++) {
alarm = String(event.alarms[i]).split(':');
$('select.edit-alarm-type').val(alarm[0]);
if (alarm[1].match(/@(\d+)/)) {
var ondate = new Date(parseInt(RegExp.$1));
$('select.edit-alarm-offset').val('@');
$('input.edit-alarm-date').val($.fullCalendar.formatDate(ondate, settings['date_format']));
$('input.edit-alarmtime').val($.fullCalendar.formatDate(ondate, settings['time_format']));
}
else if (alarm[1].match(/([-+])(\d+)([mhd])/)) {
$('input.edit-alarm-value').val(RegExp.$2);
$('select.edit-alarm-offset').val(''+RegExp.$1+RegExp.$3);
}
}
// set correct visibility by triggering onchange handlers
$('select.edit-alarm-type, select.edit-alarm-offset').change();
}
// set recurrence form
var recurrence = $('#edit-recurrence-frequency').val(event.recurrence ? event.recurrence.FREQ : '').change();
var interval = $('select.edit-recurrence-interval').val(event.recurrence ? event.recurrence.INTERVAL : 1);
var rrtimes = $('#edit-recurrence-repeat-times').val(event.recurrence ? event.recurrence.COUNT : 1);
var rrenddate = $('#edit-recurrence-enddate').val(event.recurrence && event.recurrence.UNTIL ? $.fullCalendar.formatDate(new Date(event.recurrence.UNTIL*1000), settings['date_format']) : '');
$('input.edit-recurrence-until:checked').prop('checked', false);
var weekdays = ['SU','MO','TU','WE','TH','FR','SA'];
var rrepeat_id = '#edit-recurrence-repeat-forever';
if (event.recurrence && event.recurrence.COUNT) rrepeat_id = '#edit-recurrence-repeat-count';
else if (event.recurrence && event.recurrence.UNTIL) rrepeat_id = '#edit-recurrence-repeat-until';
$(rrepeat_id).prop('checked', true);
if (event.recurrence && event.recurrence.BYDAY && event.recurrence.FREQ == 'WEEKLY') {
var wdays = event.recurrence.BYDAY.split(',');
$('input.edit-recurrence-weekly-byday').val(wdays);
}
else if (event.start) {
$('input.edit-recurrence-weekly-byday').val([weekdays[event.start.getDay()]]);
}
if (event.recurrence && event.recurrence.BYMONTHDAY) {
$('input.edit-recurrence-monthly-bymonthday').val(String(event.recurrence.BYMONTHDAY).split(','));
$('input.edit-recurrence-monthly-mode').val(['BYMONTHDAY']);
}
else if (event.start) {
$('input.edit-recurrence-monthly-bymonthday').val([event.start.getDate()]);
}
if (event.recurrence && event.recurrence.BYDAY && (event.recurrence.FREQ == 'MONTHLY' || event.recurrence.FREQ == 'YEARLY')) {
var byday, section = event.recurrence.FREQ.toLowerCase();
if ((byday = String(event.recurrence.BYDAY).match(/(-?[1-4])([A-Z]+)/))) {
$('#edit-recurrence-'+section+'-prefix').val(byday[1]);
$('#edit-recurrence-'+section+'-byday').val(byday[2]);
}
$('input.edit-recurrence-'+section+'-mode').val(['BYDAY']);
}
else if (event.start) {
$('#edit-recurrence-monthly-byday').val(weekdays[event.start.getDay()]);
}
if (event.recurrence && event.recurrence.BYMONTH) {
$('input.edit-recurrence-yearly-bymonth').val(String(event.recurrence.BYMONTH).split(','));
}
else if (event.start) {
$('input.edit-recurrence-yearly-bymonth').val([String(event.start.getMonth()+1)]);
}
// buttons
var buttons = {};
buttons[rcmail.gettext('save', 'calendar')] = function() {
var start = me.parse_datetime(starttime.val(), startdate.val());
var end = me.parse_datetime(endtime.val(), enddate.val());
// post data to server
var data = {
action: action,
start: start.getTime()/1000,
end: end.getTime()/1000,
allday: allday.checked?1:0,
title: title.val(),
description: description.val(),
location: location.val(),
categories: categories.val(),
free_busy: freebusy.val(),
priority: priority.val(),
recurrence: '',
alarms:'',
};
// serialize alarm settings
// TODO: support multiple alarm entries
var alarm = $('select.edit-alarm-type').val();
if (alarm) {
var val, offset = $('select.edit-alarm-offset').val();
if (offset == '@')
data.alarms = alarm + ':@' + (me.parse_datetime($('input.edit-alarm-time').val(), $('input.edit-alarm-date').val()).getTime()/1000);
else if ((val = parseInt($('input.edit-alarm-value').val())) && !isNaN(val) && val >= 0)
data.alarms = alarm + ':' + offset[0] + val + offset[1];
}
// gather recurrence settings
var freq;
if ((freq = recurrence.val()) != '') {
data.recurrence = {
FREQ: freq,
INTERVAL: $('#edit-recurrence-interval-'+freq.toLowerCase()).val()
};
var until = $('input.edit-recurrence-until:checked').val();
if (until == 'count')
data.recurrence.COUNT = rrtimes.val();
else if (until == 'until')
data.recurrence.UNTIL = me.parse_datetime(endtime.val(), rrenddate.val()).getTime()/1000;
if (freq == 'WEEKLY') {
var byday = [];
$('input.edit-recurrence-weekly-byday:checked').each(function(){ byday.push(this.value); });
data.recurrence.BYDAY = byday.join(',');
}
else if (freq == 'MONTHLY') {
var mode = $('input.edit-recurrence-monthly-mode:checked').val(), bymonday = [];
if (mode == 'BYMONTHDAY') {
$('input.edit-recurrence-monthly-bymonthday:checked').each(function(){ bymonday.push(this.value); });
data.recurrence.BYMONTHDAY = bymonday.join(',');
}
else
data.recurrence.BYDAY = $('#edit-recurrence-monthly-prefix').val() + $('#edit-recurrence-monthly-byday').val();
}
else if (freq == 'YEARLY') {
var byday, bymonth = [];
$('input.edit-recurrence-yearly-bymonth:checked').each(function(){ bymonth.push(this.value); });
data.recurrence.BYMONTH = bymonth.join(',');
if ((byday = $('#edit-recurrence-yearly-byday').val()))
data.recurrence.BYDAY = $('#edit-recurrence-yearly-prefix').val() + byday;
}
}
if (event.id)
data.id = event.id;
else
data.calendar = calendars.val();
rcmail.http_post('plugin.event', { e:data });
$dialog.dialog("close");
};
if (event.id) {
buttons[rcmail.gettext('remove', 'calendar')] = function() {
me.delete_event(event);
$dialog.dialog('close');
};
}
buttons[rcmail.gettext('cancel', 'calendar')] = function() {
$dialog.dialog("close");
};
// show/hide tabs according to calendar's feature support
$('#edit-tab-attendees')[(calendar.attendees?'show':'hide')]();
$('#edit-tab-attachments')[(calendar.attachments?'show':'hide')]();
// activate the first tab
$('#eventtabs').tabs('select', 0);
// open jquery UI dialog
$dialog.dialog({
modal: true,
resizable: true,
title: rcmail.gettext((action == 'edit' ? 'edit_event' : 'new_event'), 'calendar'),
close: function() {
$dialog.dialog("destroy");
$dialog.hide();
},
buttons: buttons,
minWidth: 440,
width: 480
}).show();
title.select();
};
// 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) {
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;
};
// general datepicker settings
this.datepicker_settings = {
// translate from fullcalendar format to datepicker format
dateFormat: settings['date_format'].replace(/M/g, 'm').replace(/mmmmm/, 'MM').replace(/mmm/, 'M').replace(/dddd/, 'DD').replace(/ddd/, 'D').replace(/yy/g, 'y'),
firstDay : settings['first_day'],
dayNamesMin: settings['days_short'],
monthNames: settings['months'],
monthNamesShort: settings['months'],
changeMonth: false,
showOtherMonths: true,
selectOtherMonths: true,
};
// from time and date strings to a real date object
this.parse_datetime = function(time, date) {
// we use the utility function from datepicker to parse dates
var date = $.datepicker.parseDate(me.datepicker_settings.dateFormat, date, me.datepicker_settings);
var time_arr = time.split(/[:.]/);
if (!isNaN(time_arr[0])) date.setHours(time_arr[0]);
if (!isNaN(time_arr[1])) date.setMinutes(time_arr[1]);
return date;
};
// public method to bring up the new event dialog
this.add_event = function() {
if (this.selected_calendar) {
var now = new Date();
var date = $('#calendar').fullCalendar('getDate') || now;
date.setHours(now.getHours()+1);
date.setMinutes(0);
var end = new Date(date.getTime());
end.setHours(date.getHours()+1);
event_edit_dialog('new', { start:date, end:end, allDay:false, calendar:this.selected_calendar });
}
};
// delete the given event after showing a confirmation dialog
this.delete_event = function(event) {
// send remove request to plugin
if (confirm(rcmail.gettext('deleteventconfirm', 'calendar'))) {
rcmail.http_post('plugin.event', { e:{ action:'remove', id:event.id } });
return true;
}
return false;
};
// create list of event sources AKA calendars
this.calendars = {};
var li, cal, event_sources = [];
for (var id in rcmail.env.calendars) {
cal = rcmail.env.calendars[id];
this.calendars[id] = $.extend({
events: function(start, end, callback) { load_events(id, start, end, callback); },
editable: !cal.readonly,
className: 'fc-event-cal-'+id,
id: id
}, cal);
event_sources.push(this.calendars[id]);
// init event handler on calendar list checkbox
if ((li = rcmail.get_folder_li(id, 'rcmlical'))) {
$('#'+li.id+' input').click(function(e){
var id = $(this).data('id');
if (me.calendars[id]) { // add or remove event source on click
var action = this.checked ? 'addEventSource' : 'removeEventSource';
$('#calendar').fullCalendar(action, me.calendars[id]);
}
}).data('id', id);
$(li).click(function(e){
rcmail.select_folder(id, me.selected_calendar, 'rcmlical');
me.selected_calendar = $(this).data('id');
}).data('id', id);
}
if (!cal.readonly) {
this.selected_calendar = id;
rcmail.enable_command('plugin.addevent', true);
}
}
// initalize the fullCalendar plugin
$('#calendar').fullCalendar({
header: {
left: 'prev,next today',
center: 'title',
right: 'agendaDay,agendaWeek,month'
},
aspectRatio: 1,
height: $(window).height() - 95,
eventSources: event_sources,
monthNames : settings['months'],
monthNamesShort : settings['months_short'],
dayNames : settings['days'],
dayNamesShort : settings['days_short'],
firstDay : settings['first_day'],
firstHour : settings['first_hour'],
slotMinutes : 60/settings['timeslots'],
timeFormat: settings['time_format'],
axisFormat : settings['time_format'],
columnFormat: {
month: 'ddd', // Mon
week: 'ddd ' + settings['date_short'], // Mon 9/7
day: 'dddd ' + settings['date_short'] // Monday 9/7
},
defaultView: 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')
},
selectable: true,
selectHelper: true,
loading : function(isLoading) {
this._rc_loading = rcmail.set_busy(isLoading, 'loading', this._rc_loading);
},
// event rendering
eventRender: function(event, element, view) {
if(view.name != "month") {
if (event.categories) {
if(!event.allDay)
element.find('span.fc-event-title').after('<span class="fc-event-categories">' + event.categories + '</span>');
}
if (event.location) {
element.find('span.fc-event-title').after('<span class="fc-event-location">@' + event.location + '</span>');
}
if (event.description) {
if (!event.allDay){
element.find('span.fc-event-title').after('<span class="fc-event-description">' + event.description + '</span>');
}
}
}
},
// callback for date range selection
select: function(start, end, allDay, e, view) {
var range_select = (!allDay || start.getDate() != end.getDate())
if (dialog_check(e) && range_select)
event_edit_dialog('new', { start:start, end:end, allDay:allDay, calendar:me.selected_calendar });
if (range_select || ignore_click)
view.calendar.unselect();
},
// callback for clicks in all-day box
dayClick: function(date, allDay, e, view) {
var now = new Date().getTime();
if (now - day_clicked < 400) // emulate double-click on day
event_edit_dialog('new', { start:date, end:date, allDay:allDay, calendar:me.selected_calendar });
day_clicked = now;
if (!ignore_click) {
view.calendar.gotoDate(date);
fullcalendar_update();
}
},
// callback when a specific event is clicked
eventClick : function(event) {
event_show_dialog(event);
},
// callback when an event was dragged and finally dropped
eventDrop: function(event, dayDelta, minuteDelta, allDay, revertFunc) {
if (event.end == null) {
event.end = event.start;
}
// send move request to server
var data = {
action: 'move',
id: event.id,
start: event.start.getTime()/1000,
end: event.end.getTime()/1000,
allday: allDay?1:0
};
rcmail.http_post('plugin.event', { e:data });
},
// callback for event resizing
eventResize : function(event, delta) {
// send resize request to server
var data = {
action: 'resize',
id: event.id,
start: event.start.getTime()/1000,
end: event.end.getTime()/1000,
};
rcmail.http_post('plugin.event', { e:data });
}
});
// event handler for clicks on calendar week cell of the datepicker widget
var init_week_events = function(){
$('#datepicker table.ui-datepicker-calendar td.ui-datepicker-week-col').click(function(e){
var base_date = $("#datepicker").datepicker('getDate');
var day_off = base_date.getDay() - 1;
if (day_off < 0) day_off = 6;
var base_kw = $.datepicker.iso8601Week(base_date);
var kw = parseInt($(this).html());
var diff = (kw - base_kw) * 7 * 86400000;
// select monday of the chosen calendar week
var date = new Date(base_date.getTime() - day_off * 86400000 + diff);
$('#calendar').fullCalendar('gotoDate', date).fullCalendar('setDate', date).fullCalendar('changeView', 'agendaWeek');
$("#datepicker").datepicker('setDate', date);
window.setTimeout(init_week_events, 10);
}).css('cursor', 'pointer');
};
// initialize small calendar widget using jQuery UI datepicker
$('#datepicker').datepicker($.extend(this.datepicker_settings, {
inline: true,
showWeek: true,
changeMonth: false, // maybe enable?
changeYear: false, // maybe enable?
onSelect: function(dateText, inst) {
ignore_click = true;
var d = $("#datepicker").datepicker('getDate'); //parse_datetime('0:0', dateText);
$('#calendar').fullCalendar('gotoDate', d).fullCalendar('select', d, d, true);
window.setTimeout(init_week_events, 10);
},
onChangeMonthYear: function(year, month, inst) {
window.setTimeout(init_week_events, 10);
var d = $("#datepicker").datepicker('getDate');
d.setYear(year);
d.setMonth(month - 1);
$("#datepicker").data('year', year).data('month', month);
//$('#calendar').fullCalendar('gotoDate', d).fullCalendar('setDate', d);
},
}));
window.setTimeout(init_week_events, 10);
// react on fullcalendar buttons
var fullcalendar_update = function() {
var d = $('#calendar').fullCalendar('getDate');
$("#datepicker").datepicker('setDate', d);
window.setTimeout(init_week_events, 10);
};
$("#calendar .fc-button-prev").click(fullcalendar_update);
$("#calendar .fc-button-next").click(fullcalendar_update);
$("#calendar .fc-button-today").click(fullcalendar_update);
// hide event dialog when clicking somewhere into document
$(document).bind('mousedown', dialog_check);
} // end rcube_calendar class
// configure toobar buttons
rcmail.register_command('plugin.addevent', function(){ cal.add_event(); }, true);
// export events
rcmail.register_command('plugin.export', function(){ rcmail.goto_url('plugin.export_events', { source:cal.selected_calendar }); }, true);
rcmail.enable_command('plugin.export', true);
// reload calendar
rcmail.addEventListener('plugin.reload_calendar', reload_calendar);
function reload_calendar() {
$('#calendar').fullCalendar('refetchEvents');
}
var formattime = function(hour, minutes) {
return ((hour < 10) ? "0" : "") + hour + ((minutes < 10) ? ":0" : ":") + minutes;
};
// if start date is changed, shift end date according to initial duration
var shift_enddate = function(dateText) {
var newstart = cal.parse_datetime('0', dateText);
var newend = new Date(newstart.getTime() + $('#edit-startdate').data('duration') * 1000);
$('#edit-enddate').val($.fullCalendar.formatDate(newend, cal.settings['date_format']));
};
// let's go
var cal = new rcube_calendar(rcmail.env.calendar_settings);
$(window).resize(function() {
$('#calendar').fullCalendar('option', 'height', $(window).height() - 95);
}).resize();
// show toolbar
$('#toolbar').show();
// init event dialog
$('#eventtabs').tabs();
$('#edit-enddate, input.edit-alarm-date').datepicker(cal.datepicker_settings);
$('#edit-startdate').datepicker(cal.datepicker_settings).datepicker('option', 'onSelect', shift_enddate).change(function(){ shift_enddate(this.value); });
$('#edit-allday').click(function(){ $('#edit-starttime, #edit-endtime')[(this.checked?'hide':'show')](); });
// configure drop-down menu on time input fields based on jquery UI autocomplete
$('#edit-starttime, #edit-endtime, input.edit-alarm-time')
.attr('autocomplete', "off")
.autocomplete({
delay: 100,
minLength: 1,
source: function(p, callback) {
/* Time completions */
var result = [];
var now = new Date();
var full = p.term - 1 > 0 || p.term.length > 1;
var hours = full? p.term - 0 : now.getHours();
var step = 15;
var minutes = hours * 60 + (full ? 0 : now.getMinutes());
var min = Math.ceil(minutes / step) * step % 60;
var hour = Math.floor(Math.ceil(minutes / step) * step / 60);
// list hours from 0:00 till now
for (var h = 0; h < hours; h++)
result.push(formattime(h, 0));
// list 15min steps for the next two hours
for (; h < hour + 2; h++) {
while (min < 60) {
result.push(formattime(h, min));
min += step;
}
min = 0;
}
// list the remaining hours till 23:00
while (h < 24)
result.push(formattime((h++), 0));
return callback(result);
},
open: function(event, ui) {
// scroll to current time
var widget = $(this).autocomplete('widget');
var menu = $(this).data('autocomplete').menu;
var val = $(this).val();
var li, html, offset = 0;
widget.children().each(function(){
li = $(this);
html = li.children().first().html();
if (html < val)
offset += li.height();
if (html == val)
menu.activate($.Event({ type: 'mouseenter' }), li);
});
widget.scrollTop(offset - 1);
}
})
.click(function() { // show drop-down upon clicks
$(this).autocomplete('search', $(this).val() ? $(this).val().replace(/\D.*/, "") : " ");
});
// register events on alarm fields
$('select.edit-alarm-type').change(function(){
$(this).parent().find('span.edit-alarm-values')[(this.selectedIndex>0?'show':'hide')]();
});
$('select.edit-alarm-offset').change(function(){
var mode = $(this).val() == '@' ? 'show' : 'hide';
$(this).parent().find('.edit-alarm-date, .edit-alarm-time')[mode]();
$(this).parent().find('.edit-alarm-value').prop('disabled', mode == 'show');
});
// toggle recurrence frequency forms
$('#edit-recurrence-frequency').change(function(e){
var freq = $(this).val().toLowerCase();
$('.recurrence-form').hide();
if (freq)
$('#recurrence-form-'+freq+', #recurrence-form-until').show();
});
$('#edit-recurrence-enddate').datepicker(cal.datepicker_settings).click(function(){ $("#edit-recurrence-repeat-until").prop('checked', true) });
// avoid unselecting all weekdays, monthdays and months
$('input.edit-recurrence-weekly-byday, input.edit-recurrence-monthly-bymonthday, input.edit-recurrence-yearly-bymonth').click(function(){
if (!$('input.'+this.className+':checked').length)
this.checked = true;
});
// initialize sidebar toggle
$('#sidebartoggle').click(function() {
var width = $(this).data('sidebarwidth');
var offset = $(this).data('offset');
var $sidebar = $('#sidebar'), time = 250;
if ($sidebar.is(':visible')) {
$sidebar.animate({ left:'-'+(width+10)+'px' }, time, function(){ $('#sidebar').hide(); });
$(this).animate({ left:'6px'}, time, function(){ $('#sidebartoggle').addClass('sidebarclosed') });
$('#calendar').animate({ left:'20px'}, time, function(){ $(this).fullCalendar('render'); });
}
else {
$sidebar.show().animate({ left:'10px' }, time);
$(this).animate({ left:offset+'px'}, time, function(){ $('#sidebartoggle').removeClass('sidebarclosed'); });
$('#calendar').animate({ left:(width+20)+'px'}, time, function(){ $(this).fullCalendar('render'); });
}
})
.data('offset', $('#sidebartoggle').position().left)
.data('sidebarwidth', $('#sidebar').width() + $('#sidebar').position().left);
});

View file

@ -0,0 +1,450 @@
<?php
/*
+-------------------------------------------------------------------------+
| Calendar plugin for Roundcube |
| Version 0.3 beta |
| |
| This program is free software; you can redistribute it and/or modify |
| it under the terms of the GNU General Public License version 2 |
| as published by the Free Software Foundation. |
| |
| 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 General Public License for more details. |
| |
| You should have received a copy of the GNU General Public License along |
| with this program; if not, write to the Free Software Foundation, Inc., |
| 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
| |
+-------------------------------------------------------------------------+
| Author: Lazlo Westerhof <hello@lazlo.me> |
| Thomas Bruederli <roundcube@gmail.com> |
+-------------------------------------------------------------------------+
*/
class calendar extends rcube_plugin
{
public $task = '?(?!login|logout).*';
public $rc;
public $driver;
public $ical;
public $ui;
/**
* Plugin initialization.
*/
function init()
{
$this->rc = rcmail::get_instance();
$this->register_task('calendar', 'calendar');
// load calendar configuration
if(file_exists($this->home . "/config.inc.php")) {
$this->load_config('config.inc.php');
} else {
$this->load_config('config.inc.php.dist');
}
// load localizations
$this->add_texts('localization/', true);
// load Calendar user interface which includes jquery-ui
$this->require_plugin('jqueryui');
require('lib/calendar_ui.php');
$this->ui = new calendar_ui($this);
$this->ui->init();
$skin = $this->rc->config->get('skin');
$this->include_stylesheet('skins/' . $skin . '/calendar.css');
if ($this->rc->task == 'calendar') {
$this->load_driver();
// load iCalendar functions
require('lib/calendar_ical.php');
$this->ical = new calendar_ical($this->rc, $this->driver);
// register calendar actions
$this->register_action('index', array($this, 'calendar_view'));
$this->register_action('plugin.calendar', array($this, 'calendar_view'));
$this->register_action('plugin.load_events', array($this, 'load_events'));
$this->register_action('plugin.event', array($this, 'event'));
$this->register_action('plugin.export_events', array($this, 'export_events'));
// set user's timezone
if ($this->rc->config->get('timezone') === 'auto')
$this->timezone = isset($_SESSION['timezone']) ? $_SESSION['timezone'] : date('Z');
else
$this->timezone = ($this->rc->config->get('timezone') + intval($this->rc->config->get('dst_active')));
$this->gmt_offset = $this->timezone * 3600;
}
else if ($this->rc->task == 'settings') {
$this->load_driver();
// add hooks for Calendar settings
$this->add_hook('preferences_sections_list', array($this, 'preferences_sections_list'));
$this->add_hook('preferences_list', array($this, 'preferences_list'));
$this->add_hook('preferences_save', array($this, 'preferences_save'));
}
}
private function load_driver()
{
$driver_name = $this->rc->config->get('calendar_driver', 'database');
$driver_class = $driver_name . '_driver';
require_once('drivers/calendar_driver.php');
require_once('drivers/' . $driver_name . '/' . $driver_class . '.php');
switch ($driver_name) {
case "kolab":
$this->require_plugin('kolab_core');
default:
$this->driver = new $driver_class($this);
break;
}
}
function calendar_view()
{
$this->rc->output->set_pagetitle($this->gettext('calendar'));
// Add CSS stylesheets to the page header
$this->ui->addCSS();
// Add JS files to the page header
$this->ui->addJS();
$this->register_handler('plugin.calendar_css', array($this->ui, 'calendar_css'));
$this->register_handler('plugin.calendar_list', array($this->ui, 'calendar_list'));
$this->register_handler('plugin.calendar_select', array($this->ui, 'calendar_select'));
$this->register_handler('plugin.category_select', array($this->ui, 'category_select'));
$this->register_handler('plugin.freebusy_select', array($this->ui, 'freebusy_select'));
$this->register_handler('plugin.priority_select', array($this->ui, 'priority_select'));
$this->register_handler('plugin.alarm_select', array($this->ui, 'alarm_select'));
$this->register_handler('plugin.recurrence_form', array($this->ui, 'recurrence_form'));
$this->rc->output->set_env('calendar_settings', $this->load_settings());
$this->rc->output->add_label('low','normal','high');
$this->rc->output->send("calendar.calendar");
}
/**
* Handler for preferences_sections_list hook.
* Adds Calendar settings sections into preferences sections list.
*
* @param array Original parameters
* @return array Modified parameters
*/
function preferences_sections_list($p)
{
$p['list']['calendar'] = array(
'id' => 'calendar', 'section' => $this->gettext('calendar'),
);
return $p;
}
/**
* Handler for preferences_list hook.
* Adds options blocks into Calendar settings sections in Preferences.
*
* @param array Original parameters
* @return array Modified parameters
*/
function preferences_list($p)
{
if ($p['section'] == 'calendar') {
$p['blocks']['view']['name'] = $this->gettext('mainoptions');
$field_id = 'rcmfd_default_view';
$select = new html_select(array('name' => '_default_view', 'id' => $field_id));
$select->add($this->gettext('day'), "agendaDay");
$select->add($this->gettext('week'), "agendaWeek");
$select->add($this->gettext('month'), "month");
$p['blocks']['view']['options']['default_view'] = array(
'title' => html::label($field_id, Q($this->gettext('default_view'))),
'content' => $select->show($this->rc->config->get('calendar_default_view', "agendaWeek")),
);
$field_id = 'rcmfd_time_format';
$choices = array('HH:mm', 'H:mm', 'h:mmt');
$select = new html_select(array('name' => '_time_format', 'id' => $field_id));
$select->add($choices);
$p['blocks']['view']['options']['time_format'] = array(
'title' => html::label($field_id, Q($this->gettext('time_format'))),
'content' => $select->show($this->rc->config->get('calendar_time_format', "HH:mm")),
);
$field_id = 'rcmfd_timeslot';
$choices = array('1', '2', '3', '4', '6');
$select = new html_select(array('name' => '_timeslots', 'id' => $field_id));
$select->add($choices);
$p['blocks']['view']['options']['timeslots'] = array(
'title' => html::label($field_id, Q($this->gettext('timeslots'))),
'content' => $select->show($this->rc->config->get('calendar_timeslots', 2)),
);
$field_id = 'rcmfd_timeslot';
$select = new html_select(array('name' => '_first_day', 'id' => $field_id));
$select->add(rcube_label('sunday'), '0');
$select->add(rcube_label('monday'), '1');
$select->add(rcube_label('tuesday'), '2');
$select->add(rcube_label('wednesday'), '3');
$select->add(rcube_label('thursday'), '4');
$select->add(rcube_label('friday'), '5');
$select->add(rcube_label('saturday'), '6');
$p['blocks']['view']['options']['first_day'] = array(
'title' => html::label($field_id, Q($this->gettext('first_day'))),
'content' => $select->show($this->rc->config->get('calendar_first_day', 1)),
);
// category definitions
$p['blocks']['categories']['name'] = $this->gettext('categories');
$categories = $this->rc->config->get('calendar_categories', array());
$categories_list = '';
foreach ($categories as $name => $color){
$key = md5($name);
$field_class = 'rcmfd_category_' . str_replace(' ', '_', $name);
$category_remove = new html_inputfield(array('type' => 'button', 'value' => 'X', 'class' => 'button', 'onclick' => '$(this).parent().remove()', 'title' => $this->gettext('remove_category')));
$category_name = new html_inputfield(array('name' => "_categories[$key]", 'class' => $field_class, 'size' => 30));
$category_color = new html_inputfield(array('name' => "_colors[$key]", 'class' => $field_class, 'size' => 6));
$categories_list .= html::div(null, $category_name->show($name) . '&nbsp;' . $category_color->show($color) . '&nbsp;' . $category_remove->show());
}
$p['blocks']['categories']['options']['category_' . $name] = array(
'content' => html::div(array('id' => 'calendarcategories'), $categories_list),
);
$field_id = 'rcmfd_new_category';
$new_category = new html_inputfield(array('name' => '_new_category', 'id' => $field_id, 'size' => 30));
$add_category = new html_inputfield(array('type' => 'button', 'class' => 'button', 'value' => $this->gettext('add_category'), 'onclick' => "rcube_calendar_add_category()"));
$p['blocks']['categories']['options']['categories'] = array(
'content' => $new_category->show('') . '&nbsp;' . $add_category->show(),
);
$this->rc->output->add_script('function rcube_calendar_add_category(){
var name = $("#rcmfd_new_category").val();
if (name.length) {
var input = $("<input>").attr("type", "text").attr("name", "_categories[]").attr("size", 30).val(name);
var color = $("<input>").attr("type", "text").attr("name", "_colors[]").attr("size", 6).val("000000");
var button = $("<input>").attr("type", "button").attr("value", "X").addClass("button").click(function(){ $(this).parent().remove() });
$("<div>").append(input).append("&nbsp;").append(color).append("&nbsp;").append(button).appendTo("#calendarcategories");
}
}');
}
return $p;
}
/**
* Handler for preferences_save hook.
* Executed on Calendar settings form submit.
*
* @param array Original parameters
* @return array Modified parameters
*/
function preferences_save($p)
{
if ($p['section'] == 'calendar') {
// categories
$old_categories = $new_categories = array();
foreach ($this->driver->list_categories() as $name => $color) {
$old_categories[md5($name)] = $name;
}
$categories = get_input_value('_categories', RCUBE_INPUT_POST);
$colors = get_input_value('_colors', RCUBE_INPUT_POST);
foreach ($categories as $key => $name) {
$color = preg_replace('/^#/', '', strval($colors[$key]));
// rename categories in existing events -> driver's job
if ($oldname = $old_categories[$key]) {
$this->driver->replace_category($oldname, $name, $color);
unset($old_categories[$key]);
}
else
$this->driver->add_category($name, $color);
$new_categories[$name] = $color;
}
// these old categories have been removed, alter events accordingly -> driver's job
foreach ((array)$old_categories[$key] as $key => $name) {
$this->driver->remove_category($name);
}
$p['prefs'] = array(
'calendar_default_view' => get_input_value('_default_view', RCUBE_INPUT_POST),
'calendar_time_format' => get_input_value('_time_format', RCUBE_INPUT_POST),
'calendar_timeslots' => get_input_value('_timeslots', RCUBE_INPUT_POST),
'calendar_first_day' => get_input_value('_first_day', RCUBE_INPUT_POST),
'calendar_categories' => $new_categories,
);
}
return $p;
}
function event()
{
$event = get_input_value('e', RCUBE_INPUT_POST);
$success = false;
switch ($event['action']) {
case "new":
// create UID for new event
$events['uid'] = strtoupper(md5(time() . uniqid(rand())) . '-' . substr(md5($this->rc->user->get_username()), 0, 16));
$success = $this->driver->new_event($event);
break;
case "edit":
$success = $this->driver->edit_event($event);
break;
case "resize":
$success = $this->driver->resize_event($event);
break;
case "move":
$success = $this->driver->move_event($event);
break;
case "remove":
$success = $this->driver->remove_event($event);
break;
}
if ($success) {
$this->rc->output->command('plugin.reload_calendar', array());
}
else {
$this->rc->output->show_message('calendar.errorsaving', 'error');
}
}
function load_events()
{
$events = $this->driver->load_events(get_input_value('start', RCUBE_INPUT_GET), get_input_value('end', RCUBE_INPUT_GET), get_input_value('source', RCUBE_INPUT_GET));
echo $this->encode($events);
exit;
}
function export_events()
{
$start = get_input_value('start', RCUBE_INPUT_GET);
$end = get_input_value('end', RCUBE_INPUT_GET);
if (!$start) $start = mktime(0, 0, 0, 1, date('n'), date('Y')-1);
if (!$end) $end = mktime(0, 0, 0, 31, 12, date('Y')+10);
$events = $this->driver->load_events($start, $end, get_input_value('source', RCUBE_INPUT_GET));
header("Content-Type: text/calendar");
header("Content-Disposition: inline; filename=calendar.ics");
echo $this->ical->export($events);
exit;
}
function load_settings()
{
$settings = array();
// configuration
$settings['default_view'] = (string)$this->rc->config->get('calendar_default_view', "agendaWeek");
$settings['date_format'] = (string)$this->rc->config->get('calendar_date_format', "yyyy/MM/dd");
$settings['date_short'] = (string)$this->rc->config->get('calendar_date_short', "M/d");
$settings['time_format'] = (string)$this->rc->config->get('calendar_time_format', "HH:mm");
$settings['timeslots'] = (int)$this->rc->config->get('calendar_timeslots', 2);
$settings['first_day'] = (int)$this->rc->config->get('calendar_first_day', 1);
$settings['first_hour'] = (int)$this->rc->config->get('calendar_first_hour', 6);
$settings['timezone'] = $this->timezone;
// localization
$settings['days'] = array(
rcube_label('sunday'), rcube_label('monday'),
rcube_label('tuesday'), rcube_label('wednesday'),
rcube_label('thursday'), rcube_label('friday'),
rcube_label('saturday')
);
$settings['days_short'] = array(
rcube_label('sun'), rcube_label('mon'),
rcube_label('tue'), rcube_label('wed'),
rcube_label('thu'), rcube_label('fri'),
rcube_label('sat')
);
$settings['months'] = array(
$this->rc->gettext('longjan'), $this->rc->gettext('longfeb'),
$this->rc->gettext('longmar'), $this->rc->gettext('longapr'),
$this->rc->gettext('longmay'), $this->rc->gettext('longjun'),
$this->rc->gettext('longjul'), $this->rc->gettext('longaug'),
$this->rc->gettext('longsep'), $this->rc->gettext('longoct'),
$this->rc->gettext('longnov'), $this->rc->gettext('longdec')
);
$settings['months_short'] = array(
$this->rc->gettext('jan'), $this->rc->gettext('feb'),
$this->rc->gettext('mar'), $this->rc->gettext('apr'),
$this->rc->gettext('may'), $this->rc->gettext('jun'),
$this->rc->gettext('jul'), $this->rc->gettext('aug'),
$this->rc->gettext('sep'), $this->rc->gettext('oct'),
$this->rc->gettext('nov'), $this->rc->gettext('dec')
);
$settings['today'] = rcube_label('today');
return $settings;
}
/**
* Convert the given time stamp to a GMT date string
*/
function toGMT($time, $user_tz = true)
{
$tz = $user_tz ? $this->gmt_offset : date('Z');
return date('Y-m-d H:i:s', $time - $tz);
}
/**
* Shift the given time stamo to a GMT time zone
*/
function toGMTTS($time, $user_tz = true)
{
$tz = $user_tz ? $this->gmt_offset : date('Z');
return $time - $tz;
}
/**
* Convert the given date string into a GMT-based time stamp
*/
function fromGMT($datetime, $user_tz = true)
{
$tz = $user_tz ? $this->gmt_offset : date('Z');
return strtotime($datetime) + $tz;
}
/**
* Encode events as JSON
*
* @param array Events as array
* @return string JSON encoded events
*/
function encode($events)
{
$json = array();
foreach ($events as $event) {
// TODO: compose a human readable string for recurrence_text
$json[] = array(
'start' => date('c', $event['start']), // ISO 8601 date (added in PHP 5)
'end' => date('c', $event['end']), // ISO 8601 date (added in PHP 5)
'description' => $event['description'],
'location' => $event['location'],
'className' => 'cat-' . asciiwords($event['categories'], true),
'allDay' => ($event['all_day'] == 1)?true:false,
) + $event;
}
return json_encode($json);
}
}

View file

@ -0,0 +1,54 @@
<?php
/*
+-------------------------------------------------------------------------+
| Configuration for the Calendar plugin |
| Version 0.3 alpha |
| |
| This program is free software; you can redistribute it and/or modify |
| it under the terms of the GNU General Public License version 2 |
| as published by the Free Software Foundation. |
| |
| 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 General Public License for more details. |
| |
| You should have received a copy of the GNU General Public License along |
| with this program; if not, write to the Free Software Foundation, Inc., |
| 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
| |
+-------------------------------------------------------------------------+
| Author: Lazlo Westerhof <hello@lazlo.me> |
+-------------------------------------------------------------------------+
*/
// backend type (database, google, kolab)
$rcmail_config['calendar_driver'] = "database";
// default calendar view (agendaDay, agendaWeek, month)
$rcmail_config['calendar_default_view'] = "agendaWeek";
// general date format
$rcmail_config['calendar_date_format'] = "yyyy-MM-dd";
// time format (HH:mm, H:mm, h:mmt)
$rcmail_config['calendar_time_format'] = "HH:mm";
// short date format (used for column titles)
$rcmail_config['calendar_date_short'] = 'M-d';
// timeslots per hour (1, 2, 3, 4, 6)
$rcmail_config['calendar_timeslots'] = 2;
// first day of the week (0-6)
$rcmail_config['calendar_first_day'] = 1;
// first hour of the calendar (0-23)
$rcmail_config['calendar_first_hour'] = 6;
// event categories
$rcmail_config['calendar_categories'] = array('Personal' => 'c0c0c0',
'Work' => 'ff0000',
'Family' => '00ff00',
'Holiday' => 'ff6600');
?>

View file

@ -0,0 +1,550 @@
<?php
/**
* A Class for connecting to a caldav server
*
* @package awl
* removed curl - now using fsockopen
* changed 2009 by Andres Obrero - Switzerland andres@obrero.ch
*
* @subpackage caldav
* @author Andrew McMillan <debian@mcmillan.net.nz>
* @copyright Andrew McMillan
* @license http://gnu.org/copyleft/gpl.html GNU GPL v2
*/
/**
* A class for accessing DAViCal via CalDAV, as a client
*
* @package awl
*/
class CalDAVClient {
/**
* Server, username, password, calendar
*
* @var string
*/
var $base_url, $user, $pass, $calendar, $entry, $protocol, $server, $port;
/**
* The useragent which is send to the caldav server
*
* @var string
*/
var $user_agent = 'DAViCalClient';
var $headers = array();
var $body = "";
var $requestMethod = "GET";
var $httpRequest = ""; // for debugging http headers sent
var $xmlRequest = ""; // for debugging xml sent
var $httpResponse = ""; // for debugging http headers received
var $xmlResponse = ""; // for debugging xml received
/**
* Constructor, initialises the class
*
* @param string $base_url The URL for the calendar server
* @param string $user The name of the user logging in
* @param string $pass The password for that user
* @param string $calendar The name of the calendar (not currently used)
*/
function CalDAVClient( $base_url, $user, $pass, $calendar = '' ) {
$this->user = $user;
$this->pass = $pass;
$this->calendar = $calendar;
$this->headers = array();
if ( preg_match( '#^(https?)://([a-z0-9.-]+)(:([0-9]+))?(/.*)$#', $base_url, $matches ) ) {
$this->server = $matches[2];
$this->base_url = $matches[5];
if ( $matches[1] == 'https' ) {
$this->protocol = 'ssl';
$this->port = 443;
}
else {
$this->protocol = 'tcp';
$this->port = 80;
}
if ( $matches[4] != '' ) {
$this->port = intval($matches[4]);
}
}
else {
trigger_error("Invalid URL: '".$base_url."'", E_USER_ERROR);
}
}
/**
* Adds an If-Match or If-None-Match header
*
* @param bool $match to Match or Not to Match, that is the question!
* @param string $etag The etag to match / not match against.
*/
function SetMatch( $match, $etag = '*' ) {
$this->headers[] = sprintf( "%s-Match: %s", ($match ? "If" : "If-None"), $etag);
}
/**
* Add a Depth: header. Valid values are 0, 1 or infinity
*
* @param int $depth The depth, default to infinity
*/
function SetDepth( $depth = '0' ) {
$this->headers[] = 'Depth: '. ($depth == '1' ? "1" : ($depth == 'infinity' ? $depth : "0") );
}
/**
* Add a Depth: header. Valid values are 1 or infinity
*
* @param int $depth The depth, default to infinity
*/
function SetUserAgent( $user_agent = null ) {
if ( !isset($user_agent) ) $user_agent = $this->user_agent;
$this->user_agent = $user_agent;
}
/**
* Add a Content-type: header.
*
* @param int $type The content type
*/
function SetContentType( $type ) {
$this->headers[] = "Content-type: $type";
}
/**
* Split response into httpResponse and xmlResponse
*
* @param string Response from server
*/
function ParseResponse( $response ) {
$pos = strpos($response, '<?xml');
if ($pos === false) {
$this->httpResponse = trim($response);
}
else {
$this->httpResponse = trim(substr($response, 0, $pos));
$this->xmlResponse = trim(substr($response, $pos));
}
}
/**
* Output http request headers
*
* @return HTTP headers
*/
function GetHttpRequest() {
return $this->httpRequest;
}
/**
* Output http response headers
*
* @return HTTP headers
*/
function GetHttpResponse() {
return $this->httpResponse;
}
/**
* Output xml request
*
* @return raw xml
*/
function GetXmlRequest() {
return $this->xmlRequest;
}
/**
* Output xml response
*
* @return raw xml
*/
function GetXmlResponse() {
return $this->xmlResponse;
}
/**
* Send a request to the server
*
* @param string $relative_url The URL to make the request to, relative to $base_url
*
* @return string The content of the response from the server
*/
function DoRequest( $relative_url = "" ) {
if(!defined("_FSOCK_TIMEOUT")){ define("_FSOCK_TIMEOUT", 10); }
$headers = array();
$headers[] = $this->requestMethod." ". $this->base_url . $relative_url . " HTTP/1.1";
$headers[] = "Authorization: Basic ".base64_encode($this->user .":". $this->pass );
$headers[] = "Host: ".$this->server .":".$this->port;
foreach( $this->headers as $ii => $head ) {
$headers[] = $head;
}
$headers[] = "Content-Length: " . strlen($this->body);
$headers[] = "User-Agent: " . $this->user_agent;
$headers[] = 'Connection: close';
$this->httpRequest = join("\r\n",$headers);
$this->xmlRequest = $this->body;
$fip = fsockopen( $this->protocol . '://' . $this->server, $this->port, $errno, $errstr, _FSOCK_TIMEOUT); //error handling?
if ( !(get_resource_type($fip) == 'stream') ) return false;
if ( !fwrite($fip, $this->httpRequest."\r\n\r\n".$this->body) ) { fclose($fip); return false; }
$rsp = "";
while( !feof($fip) ) { $rsp .= fgets($fip,8192); }
fclose($fip);
$this->headers = array(); // reset the headers array for our next request
$this->ParseResponse($rsp);
return $rsp;
}
/**
* Send an OPTIONS request to the server
*
* @param string $relative_url The URL to make the request to, relative to $base_url
*
* @return array The allowed options
*/
function DoOptionsRequest( $relative_url = "" ) {
$this->requestMethod = "OPTIONS";
$this->body = "";
$headers = $this->DoRequest($relative_url);
$options_header = preg_replace( '/^.*Allow: ([a-z, ]+)\r?\n.*/is', '$1', $headers );
$options = array_flip( preg_split( '/[, ]+/', $options_header ));
return $options;
}
/**
* Send an XML request to the server (e.g. PROPFIND, REPORT, MKCALENDAR)
*
* @param string $method The method (PROPFIND, REPORT, etc) to use with the request
* @param string $xml The XML to send along with the request
* @param string $relative_url The URL to make the request to, relative to $base_url
*
* @return array An array of the allowed methods
*/
function DoXMLRequest( $request_method, $xml, $relative_url = '' ) {
$this->body = $xml;
$this->requestMethod = $request_method;
$this->SetContentType("text/xml");
return $this->DoRequest($relative_url);
}
/**
* Get a single item from the server.
*
* @param string $relative_url The part of the URL after the calendar
*/
function DoGETRequest( $relative_url ) {
$this->body = "";
$this->requestMethod = "GET";
return $this->DoRequest( $relative_url );
}
/**
* PUT a text/icalendar resource, returning the etag
*
* @param string $relative_url The URL to make the request to, relative to $base_url
* @param string $icalendar The iCalendar resource to send to the server
* @param string $etag The etag of an existing resource to be overwritten, or '*' for a new resource.
*
* @return string The content of the response from the server
*/
function DoPUTRequest( $relative_url, $icalendar, $etag = null ) {
$this->body = $icalendar;
$this->requestMethod = "PUT";
if ( $etag != null ) {
$this->SetMatch( ($etag != '*'), $etag );
}
$this->SetContentType("text/icalendar");
$headers = $this->DoRequest($relative_url);
/**
* RSCDS will always return the real etag on PUT. Other CalDAV servers may need
* more work, but we are assuming we are running against RSCDS in this case.
*/
$etag = preg_replace( '/^.*Etag: "?([^"\r\n]+)"?\r?\n.*/is', '$1', $headers );
return $etag;
}
/**
* DELETE a text/icalendar resource
*
* @param string $relative_url The URL to make the request to, relative to $base_url
* @param string $etag The etag of an existing resource to be deleted, or '*' for any resource at that URL.
*
* @return int The HTTP Result Code for the DELETE
*/
function DoDELETERequest( $relative_url, $etag = null ) {
$this->body = "";
$this->requestMethod = "DELETE";
if ( $etag != null ) {
$this->SetMatch( true, $etag );
}
$this->DoRequest($relative_url);
return $this->resultcode;
}
/**
* Given XML for a calendar query, return an array of the events (/todos) in the
* response. Each event in the array will have a 'href', 'etag' and '$response_type'
* part, where the 'href' is relative to the calendar and the '$response_type' contains the
* definition of the calendar data in iCalendar format.
*
* @param string $filter XML fragment which is the <filter> element of a calendar-query
* @param string $relative_url The URL relative to the base_url specified when the calendar was opened. Default ''.
* @param string $report_type Used as a name for the array element containing the calendar data. @deprecated
*
* @return array An array of the relative URLs, etags, and events from the server. Each element of the array will
* be an array with 'href', 'etag' and 'data' elements, corresponding to the URL, the server-supplied
* etag (which only varies when the data changes) and the calendar data in iCalendar format.
*/
function DoCalendarQuery( $filter, $relative_url = '' ) {
$xml = <<<EOXML
<?xml version="1.0" encoding="utf-8" ?>
<C:calendar-query xmlns:D="DAV:" xmlns:C="urn:ietf:params:xml:ns:caldav">
<D:prop>
<C:calendar-data/>
<D:getetag/>
</D:prop>$filter
</C:calendar-query>
EOXML;
$this->DoXMLRequest( 'REPORT', $xml, $relative_url );
$xml_parser = xml_parser_create_ns('UTF-8');
$this->xml_tags = array();
xml_parser_set_option ( $xml_parser, XML_OPTION_SKIP_WHITE, 1 );
xml_parse_into_struct( $xml_parser, $this->xmlResponse, $this->xml_tags );
xml_parser_free($xml_parser);
$report = array();
foreach( $this->xml_tags as $k => $v ) {
switch( $v['tag'] ) {
case 'DAV::RESPONSE':
if ( $v['type'] == 'open' ) {
$response = array();
}
elseif ( $v['type'] == 'close' ) {
$report[] = $response;
}
break;
case 'DAV::HREF':
$response['href'] = basename( $v['value'] );
break;
case 'DAV::GETETAG':
$response['etag'] = preg_replace('/^"?([^"]+)"?/', '$1', $v['value']);
break;
case 'URN:IETF:PARAMS:XML:NS:CALDAV:CALENDAR-DATA':
$response['data'] = $v['value'];
break;
}
}
return $report;
}
/**
* Get the events in a range from $start to $finish. The dates should be in the
* format yyyymmddThhmmssZ and should be in GMT. The events are returned as an
* array of event arrays. Each event array will have a 'href', 'etag' and 'event'
* part, where the 'href' is relative to the calendar and the event contains the
* definition of the event in iCalendar format.
*
* @param timestamp $start The start time for the period
* @param timestamp $finish The finish time for the period
* @param string $relative_url The URL relative to the base_url specified when the calendar was opened. Default ''.
*
* @return array An array of the relative URLs, etags, and events, returned from DoCalendarQuery() @see DoCalendarQuery()
*/
function GetEvents( $start = null, $finish = null, $relative_url = '' ) {
$filter = "";
if ( isset($start) && isset($finish) )
$range = "<C:time-range start=\"$start\" end=\"$finish\"/>";
else
$range = '';
$filter = <<<EOFILTER
<C:filter>
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
$range
</C:comp-filter>
</C:comp-filter>
</C:filter>
EOFILTER;
return $this->DoCalendarQuery($filter, $relative_url);
}
/**
* Get the todo's in a range from $start to $finish. The dates should be in the
* format yyyymmddThhmmssZ and should be in GMT. The events are returned as an
* array of event arrays. Each event array will have a 'href', 'etag' and 'event'
* part, where the 'href' is relative to the calendar and the event contains the
* definition of the event in iCalendar format.
*
* @param timestamp $start The start time for the period
* @param timestamp $finish The finish time for the period
* @param boolean $completed Whether to include completed tasks
* @param boolean $cancelled Whether to include cancelled tasks
* @param string $relative_url The URL relative to the base_url specified when the calendar was opened. Default ''.
*
* @return array An array of the relative URLs, etags, and events, returned from DoCalendarQuery() @see DoCalendarQuery()
*/
function GetTodos( $start, $finish, $completed = false, $cancelled = false, $relative_url = "" ) {
if ( $start && $finish ) {
$time_range = <<<EOTIME
<C:time-range start="$start" end="$finish"/>
EOTIME;
}
// Warning! May contain traces of double negatives...
$neg_cancelled = ( $cancelled === true ? "no" : "yes" );
$neg_completed = ( $cancelled === true ? "no" : "yes" );
$filter = <<<EOFILTER
<C:filter>
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VTODO">
<C:prop-filter name="STATUS">
<C:text-match negate-condition="$neg_completed">COMPLETED</C:text-match>
</C:prop-filter>
<C:prop-filter name="STATUS">
<C:text-match negate-condition="$neg_cancelled">CANCELLED</C:text-match>
</C:prop-filter>$time_range
</C:comp-filter>
</C:comp-filter>
</C:filter>
EOFILTER;
return $this->DoCalendarQuery($filter, $relative_url);
}
/**
* Get the calendar entry by UID
*
* @param uid
* @param string $relative_url The URL relative to the base_url specified when the calendar was opened. Default ''.
*
* @return array An array of the relative URL, etag, and calendar data returned from DoCalendarQuery() @see DoCalendarQuery()
*/
function GetEntryByUid( $uid, $relative_url = '' ) {
$filter = "";
if ( $uid ) {
$filter = <<<EOFILTER
<C:filter>
<C:comp-filter name="VCALENDAR">
<C:comp-filter name="VEVENT">
<C:prop-filter name="UID">
<C:text-match icollation="i;octet">$uid</C:text-match>
</C:prop-filter>
</C:comp-filter>
</C:comp-filter>
</C:filter>
EOFILTER;
}
return $this->DoCalendarQuery($filter, $relative_url);
}
/**
* Get the calendar entry by HREF
*
* @param string $href The href from a call to GetEvents or GetTodos etc.
* @param string $relative_url The URL relative to the base_url specified when the calendar was opened. Default ''.
*
* @return string The iCalendar of the calendar entry
*/
function GetEntryByHref( $href, $relative_url = '' ) {
return $this->DoGETRequest( $relative_url . $href );
}
}
/**
* Usage example
*
* $cal = new CalDAVClient( "http://calendar.example.com/caldav.php/username/calendar/", "username", "password", "calendar" );
* $options = $cal->DoOptionsRequest();
* if ( isset($options["PROPFIND"]) ) {
* // Fetch some information about the events in that calendar
* $cal->SetDepth(1);
* $folder_xml = $cal->DoXMLRequest("PROPFIND", '<?xml version="1.0" encoding="utf-8" ?><propfind xmlns="DAV:"><prop><getcontentlength/><getcontenttype/><resourcetype/><getetag/></prop></propfind>' );
* }
* // Fetch all events for February
* $events = $cal->GetEvents("20070101T000000Z","20070201T000000Z");
* foreach ( $events AS $k => $event ) {
* do_something_with_event_data( $event['data'] );
* }
* $acc = array();
* $acc["google"] = array(
* "user"=>"kunsttherapie@gmail.com",
* "pass"=>"xxxxx",
* "server"=>"ssl://www.google.com",
* "port"=>"443",
* "uri"=>"https://www.google.com/calendar/dav/kunsttherapie@gmail.com/events/",
* );
*
* $acc["davical"] = array(
* "user"=>"some_user",
* "pass"=>"big secret",
* "server"=>"calendar.foo.bar",
* "port"=>"80",
* "uri"=>"http://calendar.foo.bar/caldav.php/some_user/home/",
* );
* //*******************************
*
* $account = $acc["davical"];
*
* //*******************************
* $cal = new CalDAVClient( $account["uri"], $account["user"], $account["pass"], "", $account["server"], $account["port"] );
* $options = $cal->DoOptionsRequest();
* print_r($options);
*
* //*******************************
* //*******************************
*
* $xmlC = <<<PROPP
* <?xml version="1.0" encoding="utf-8" ?>
* <D:propfind xmlns:D="DAV:" xmlns:C="http://calendarserver.org/ns/">
* <D:prop>
* <D:displayname />
* <C:getctag />
* <D:resourcetype />
*
* </D:prop>
* </D:propfind>
* PROPP;
* //if ( isset($options["PROPFIND"]) ) {
* // Fetch some information about the events in that calendar
* // $cal->SetDepth(1);
* // $folder_xml = $cal->DoXMLRequest("PROPFIND", $xmlC);
* // print_r( $folder_xml);
* //}
*
* // Fetch all events for February
* $events = $cal->GetEvents("20090201T000000Z","20090301T000000Z");
* foreach ( $events as $k => $event ) {
* print_r($event['data']);
* print "\n---------------------------------------------\n";
* }
*
* //*******************************
* //*******************************
*/
?>

View file

@ -0,0 +1,158 @@
<?php
/**
* RoundCube Calendar
*
* CalDAV backend based on exemplary DAViCal / AWL client.
*
* @version 0.2 BETA 2
* @author Michael Duelli
* @url http://rc-calendar.lazlo.me
* @licence GNU GPL
* @copyright (c) 2010 Lazlo Westerhof - Netherlands
*/
require_once('caldav-client.php');
class caldav_driver extends calendar_driver
{
private $rcmail = null;
private $cal = null;
private $calendar = null;
/**
* @param object rcmail The RoundCube instance.
* @param string server The CalDAV server.
* @param string user The user name.
* @param string pass The user's password.
* @param string calendar The user calendar.
*/
public function __construct($rcmail, $server, $user, $pass, $calendar) {
$this->rcmail = $rcmail;
$this->calendar = '/' . $calendar;
$this->cal = new CalDAVClient($server. "/" . $user, $user, $pass, $calendar /* is ignored currently */);
$this->cal->setUserAgent('RoundCube');
}
public function new_event($event) {
// FIXME Implement
}
public function edit_event($event) {
// FIXME Implement
}
public function move_event($event) {
// FIXME Implement. Can be done via editEvent
}
public function resize_event($event) {
// FIXME Implement. Can be done via editEvent
}
public function remove_event($event) {
// FIXME Implement.
}
public function load_events($start, $end, $calendars = null) {
if (!empty($this->rcmail->user->ID)) {
// Fetch events.
$result = $this->cal->GetEvents($this->GMT_to_iCalendar($start), $this->GMT_to_iCalendar($end), $this->calendar);
$events = array();
foreach ($result as $k => $event) {
$lines = explode("\n", $event['data']);
$n = count($lines);
$eventid = null;
$flag = true;
for ($i = 0; $i < $n; $i++) {
if ($flag) {
if (strpos($lines[$i], "BEGIN:VEVENT") === 0)
$flag = false;
continue;
}
if (strpos($lines[$i], "END:VEVENT") === 0)
break;
if (empty($lines[$i]))
continue; // FIXME
$tmp = explode(":", $lines[$i]);
if (count($tmp) !== 2)
continue; // FIXME
list($id, $value) = $tmp;
if (!isset($id) || !isset($value))
continue; // FIXME
if (is_null($eventid) && strpos($id, "UID") === 0)
$eventid = $value;
elseif (!isset($event['start']) && strpos($id, "DTSTART") === 0) {
$event['start'] = $this->iCalendar_to_Unix($value);
// Check for all-day event.
$event['all_day'] = (strlen($value) === 8 ? 0 : 1);
} elseif (!isset($event['end']) && strpos($id, "DTEND") === 0)
$event['end'] = $this->iCalendar_to_Unix($value);
elseif (!isset($event['title']) && strpos($id, "SUMMARY") === 0)
$event['title'] = $value;
elseif (!isset($event['description']) && strpos($id, "DESCRIPTION") === 0) {
$event['description'] = $value;
// FIXME Problem with multiple lines!
// if ($i+1 < $n && $lines[$i+1] does not contain keyword...) {
// Add line to description
// $i++;
// }
} elseif (!isset($event['location']) && strpos($id, "LOCATION") === 0)
$event['location'] = $value;
elseif (!isset($event['categories']) && strpos($id, "CATEGORIES") === 0)
$event['categories'] = $value;
}
$events[]=array(
'event_id' => $eventid,
'start' => $event['start'],
'end' => $event['end'],
'title' => strval($event['title']),
'description' => strval($event['description']),
'location' => strval($event['location']),
'categories' => $event['categories'],
'allDay' => $event['all_day'],
);
}
return $events;
}
}
/**
* Convert a GMT time stamp ('Y-m-d H:i:s') to the iCalendar format as defined in
* RFC 5545, Section 3.2.19, http://tools.ietf.org/html/rfc5545#section-3.2.19.
*
* @param timestamp A GMT time stamp ('Y-m-d H:i:s')
* @return An iCalendar time stamp, e.g. yyyymmddThhmmssZ
*/
private function GMT_to_iCalendar($timestamp) {
$unix_timestamp = strtotime($timestamp);
return date("Ymd", $unix_timestamp) . "T" . date("His", $unix_timestamp) . "Z";
}
/**
* Convert a time stamp in iCalendar format as defined in
* RFC 5545, Section 3.2.19, http://tools.ietf.org/html/rfc5545#section-3.2.19
* to a Unix time stamp. Further conversion is done in jsonEvents.
*
* @param timestamp An iCalendar time stamp, e.g. yyyymmddThhmmssZ
* @return A Unix time stamp
*/
private function iCalendar_to_Unix($timestamp) {
return strtotime($timestamp);
}
}
?>

View file

@ -0,0 +1,171 @@
<?php
/*
+-------------------------------------------------------------------------+
| Driver interface for the Calendar Plugin |
| Version 0.3 beta |
| |
| This program is free software; you can redistribute it and/or modify |
| it under the terms of the GNU General Public License version 2 |
| as published by the Free Software Foundation. |
| |
| 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 General Public License for more details. |
| |
| You should have received a copy of the GNU General Public License along |
| with this program; if not, write to the Free Software Foundation, Inc., |
| 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
| |
+-------------------------------------------------------------------------+
| Author: Lazlo Westerhof <hello@lazlo.me> |
| Thomas Bruederli <roundcube@gmail.com> |
+-------------------------------------------------------------------------+
*/
abstract class calendar_driver
{
// backend features
public $attendees = false;
public $attachments = false;
/**
* Get a list of available calendars from this source
*/
abstract function list_calendars();
/**
* Create a new calendar assigned to the current user
*
* @param array Hash array with calendar properties
* name: Calendar name
* color: The color of the calendar
* @return mixed ID of the calendar on success, False on error
*/
abstract function create_calendar($prop);
/**
* Add a single event to the database
*
* @param array Hash array with vent properties:
* calendar: Calendar identifier to add event to (optional)
* uid: Unique identifier of this event
* start: Event start date/time as unix timestamp
* end: Event end date/time as unix timestamp
* allday: Boolean flag if this is an all-day event
* title: Event title/summary
* location: Location string
* description: Event description
* recurrence: Recurrence definition according to iCalendar specification
* categories: Event categories (comma-separated list)
* free_busy: Show time as free/busy/outofoffice
* priority: Event priority
* alarms: Reminder settings (TBD.)
* @return mixed New event ID on success, False on error
*/
abstract function new_event($event);
/**
* Update an event entry with the given data
*
* @see Driver:new_event()
* @return boolean True on success, False on error
*/
abstract function edit_event($event);
/**
* Move a single event
*
* @param array Hash array with event properties:
* id: Event identifier
* start: Event start date/time as unix timestamp
* end: Event end date/time as unix timestamp
* allday: Boolean flag if this is an all-day event
* @return boolean True on success, False on error
*/
abstract function move_event($event);
/**
* Resize a single event
*
* @param array Hash array with event properties:
* id: Event identifier
* start: Event start date/time as unix timestamp in user timezone
* end: Event end date/time as unix timestamp in user timezone
* @return boolean True on success, False on error
*/
abstract function resize_event($event);
/**
* Remove a single event from the database
*
* @param array Hash array with event properties:
* id: Event identifier
* @return boolean True on success, False on error
*/
abstract function remove_event($event);
/**
* Get events from source.
*
* @param integer Event's new start (unix timestamp)
* @param integer Event's new end (unix timestamp)
* @param mixed List of calendar IDs to load events from (either as array or comma-separated string)
* @return array A list of event records
*/
abstract function load_events($start, $end, $calendars = null);
/**
* Search events using the given query
*
* @param integer Event's new start (unix timestamp)
* @param integer Event's new end (unix timestamp)
* @param string Search query
* @param mixed List of calendar IDs to load events from (either as array or comma-separated string)
* @return array A list of event records
*/
abstract function search_events($start, $end, $query, $calendars = null);
/**
* Save an attachment related to the given event
*/
public function add_attachment($attachment, $event_id) { }
/**
* Remove a specific attachment from the given event
*/
public function remove_attachment($attachment, $event_id) { }
/**
* List availabale categories
* The default implementation reads them from config/user prefs
*/
public function list_categories()
{
$rcmail = rcmail::get_instance();
return $rcmail->config->get('calendar_categories', array());
}
/**
* Create a new category
*/
public function add_category($name, $color) { }
/**
* Remove the given category
*/
public function remove_category($name) { }
/**
* Update/replace a category
*/
public function replace_category($oldname, $name, $color) { }
/**
* Fetch free/busy information from a person within the given range
*/
public function get_freebusy_list($email, $start, $end)
{
return array();
}
}

View file

@ -0,0 +1,388 @@
<?php
/*
+-------------------------------------------------------------------------+
| Database driver for the Calendar Plugin |
| Version 0.3 beta |
| |
| This program is free software; you can redistribute it and/or modify |
| it under the terms of the GNU General Public License version 2 |
| as published by the Free Software Foundation. |
| |
| 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 General Public License for more details. |
| |
| You should have received a copy of the GNU General Public License along |
| with this program; if not, write to the Free Software Foundation, Inc., |
| 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
| |
+-------------------------------------------------------------------------+
| Author: Lazlo Westerhof <hello@lazlo.me> |
| Thomas Bruederli <roundcube@gmail.com> |
+-------------------------------------------------------------------------+
*/
class database_driver extends calendar_driver
{
// features this backend supports
public $attendees = true;
public $attachments = true;
private $rc;
private $cal;
private $calendars = array();
private $calendar_ids = '';
private $free_busy_map = array('free' => 0, 'busy' => 1, 'out-of-office' => 2, 'outofoffice' => 2);
/**
* Default constructor
*/
public function __construct($cal)
{
$this->cal = $cal;
$this->rc = $cal->rc;
$this->_read_calendars();
}
/**
* Read available calendars for the current user and store them internally
*/
private function _read_calendars()
{
if (!empty($this->rc->user->ID)) {
$calendar_ids = array();
$result = $this->rc->db->query(
"SELECT * FROM calendars
WHERE user_id=?",
$this->rc->user->ID
);
while ($result && ($arr = $this->rc->db->fetch_assoc($result))) {
$this->calendars[$arr['calendar_id']] = $arr;
$calendar_ids[] = $this->rc->db->quote($arr['calendar_id']);
}
$this->calendar_ids = join(',', $calendar_ids);
}
}
/**
* Get a list of available calendars from this source
*/
public function list_calendars()
{
// attempt to create a default calendar for this user
if (empty($this->calendars)) {
if ($this->create_calendar(array('name' => 'Default', 'color' => 'cc0000')))
$this->_read_calendars();
}
return $this->calendars;
}
/**
* Create a new calendar assigned to the current user
*
* @param array Hash array with calendar properties
* name: Calendar name
* color: The color of the calendar
* @return mixed ID of the calendar on success, False on error
*/
public function create_calendar($prop)
{
$result = $this->rc->db->query(
"INSERT INTO calendars
(user_id, name, color)
VALUES (?, ?, ?)",
$this->rc->user->ID,
$prop['name'],
$prop['color']
);
if ($result)
return $this->rc->db->insert_id('calendars');
return false;
}
/**
* Add a single event to the database
*
* @param array Hash array with event properties
* @see Driver:new_event()
*/
public function new_event($event)
{
if (!empty($this->calendars)) {
if ($event['calendar'] && !$this->calendars[$event['calendar']])
return false;
if (!$event['calendar'])
$event['calendar'] = reset(array_keys($this->calendars));
$event = $this->_save_preprocess($event);
$query = $this->rc->db->query(sprintf(
"INSERT INTO events
(calendar_id, created, changed, uid, start, end, all_day, recurrence, title, description, location, categories, free_busy, priority, alarms)
VALUES (?, %s, %s, ?, %s, %s, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
$this->rc->db->now(),
$this->rc->db->now(),
$this->rc->db->fromunixtime($event['start']),
$this->rc->db->fromunixtime($event['end'])
),
$event['calendar'],
strval($event['uid']),
intval($event['allday']),
$event['recurrence'],
strval($event['title']),
strval($event['description']),
strval($event['location']),
strval($event['categories']),
intval($event['free_busy']),
intval($event['priority']),
$event['alarms']
);
return $this->rc->db->insert_id('events');
}
return false;
}
/**
* Update an event entry with the given data
*
* @param array Hash array with event properties
* @see Driver:new_event()
*/
public function edit_event($event)
{
if (!empty($this->calendars)) {
$event = $this->_save_preprocess($event);
$query = $this->rc->db->query(sprintf(
"UPDATE events
SET changed=%s, start=%s, end=%s, all_day=?, recurrence=?, title=?, description=?, location=?, categories=?, free_busy=?, priority=?, alarms=?
WHERE event_id=?
AND calendar_id IN (" . $this->calendar_ids . ")",
$this->rc->db->now(),
$this->rc->db->fromunixtime($event['start']),
$this->rc->db->fromunixtime($event['end'])
),
intval($event['allday']),
$event['recurrence'],
strval($event['title']),
strval($event['description']),
strval($event['location']),
strval($event['categories']),
intval($event['free_busy']),
intval($event['priority']),
$event['alarms'],
$event['id']
);
return $this->rc->db->affected_rows($query);
}
return false;
}
/**
* Convert save data to be used in SQL statements
*/
private function _save_preprocess($event)
{
// compose vcalendar-style recurrencue rule from structured data
$rrule = '';
if (is_array($event['recurrence'])) {
foreach ($event['recurrence'] as $k => $val) {
$k = strtoupper($k);
switch ($k) {
case 'UNTIL':
$val = gmdate('Ymd\THis', $val);
break;
}
$rrule .= $k . '=' . $val . ';';
}
}
else if (is_string($event['recurrence']))
$rrule = $event['recurrence'];
$event['recurrence'] = rtrim($rrule, ';');
$event['free_busy'] = intval($this->free_busy_map[strtolower($event['free_busy'])]);
$event['allday'] = $event['allday'] ? 1 : 0;
return $event;
}
/**
* Move a single event
*
* @param array Hash array with event properties
* @see Driver:move_event()
*/
public function move_event($event)
{
if (!empty($this->calendars)) {
$query = $this->rc->db->query(sprintf(
"UPDATE events
SET changed=%s, start=%s, end=%s, all_day=?
WHERE event_id=?
AND calendar_id IN (" . $this->calendar_ids . ")",
$this->rc->db->now(),
$this->rc->db->fromunixtime($event['start']),
$this->rc->db->fromunixtime($event['end'])
),
$event['allday'] ? 1 : 0,
$event['id']
);
return $this->rc->db->affected_rows($query);
}
return false;
}
/**
* Resize a single event
*
* @param array Hash array with event properties
* @see Driver:resize_event()
*/
public function resize_event($event)
{
if (!empty($this->calendars)) {
$query = $this->rc->db->query(sprintf(
"UPDATE events
SET changed=%s, start=%s, end=%s
WHERE event_id=?
AND calendar_id IN (" . $this->calendar_ids . ")",
$this->rc->db->now(),
$this->rc->db->fromunixtime($event['start']),
$this->rc->db->fromunixtime($event['end'])
),
$event['id']
);
return $this->rc->db->affected_rows($query);
}
return false;
}
/**
* Remove a single event from the database
*
* @param array Hash array with event properties
* @see Driver:remove_event()
*/
public function remove_event($event)
{
if (!empty($this->calendars)) {
$query = $this->rc->db->query(
"DELETE FROM events
WHERE event_id=?
AND calendar_id IN (" . $this->calendar_ids . ")",
$event['id']
);
return $this->rc->db->affected_rows($query);
}
return false;
}
/**
* Get event data
*
* @see Driver:load_events()
*/
public function load_events($start, $end, $calendars = null)
{
if (empty($calendars))
$calendars = array_keys($this->calendars);
else if (is_string($calendars))
$calendars = explode(',', $calendars);
// only allow to select from calendars of this use
$calendars = array_intersect($calendars, array_keys($this->calendars));
$events = array();
$free_busy_map = array_flip($this->free_busy_map);
if (!empty($calendars)) {
$result = $this->rc->db->query(sprintf(
"SELECT * FROM events
WHERE calendar_id IN (%s)
AND start >= %s AND end <= %s",
$this->calendar_ids,
$this->rc->db->fromunixtime($start),
$this->rc->db->fromunixtime($end)
));
while ($result && ($event = $this->rc->db->fetch_assoc($result))) {
$event['id'] = $event['event_id'];
$event['start'] = strtotime($event['start']);
$event['end'] = strtotime($event['end']);
$event['free_busy'] = $free_busy_map[$event['free_busy']];
$event['calendar'] = $event['calendar_id'];
// parse recurrence rule
if ($event['recurrence'] && preg_match_all('/([A-Z]+)=([^;]+);?/', $event['recurrence'], $m, PREG_SET_ORDER)) {
$event['recurrence'] = array();
foreach ($m as $rr) {
if (is_numeric($rr[2]))
$rr[2] = intval($rr[2]);
else if ($rr[1] == 'UNTIL')
$rr[2] = strtotime($rr[2]);
$event['recurrence'][$rr[1]] = $rr[2];
}
}
unset($event['event_id'], $event['calendar_id']);
$events[] = $event;
}
}
return $events;
}
/**
* Search events
*
* @see Driver:search_events()
*/
public function search_events($start, $end, $query, $calendars = null)
{
}
/**
* Save an attachment related to the given event
*/
function add_attachment($attachment, $event_id)
{
// TBD.
return false;
}
/**
* Remove a specific attachment from the given event
*/
function remove_attachment($attachment, $event_id)
{
// TBD.
return false;
}
/**
* Remove the given category
*/
public function remove_category($name)
{
// TBD. alter events accordingly
return false;
}
/**
* Update/replace a category
*/
public function replace_category($oldname, $name, $color)
{
// TBD. alter events accordingly
return false;
}
}

View file

@ -0,0 +1,70 @@
/**
* Roundcube Calendar
*
* Plugin to add a calendar to Roundcube.
*
* @version 0.3 beta
* @author Lazlo Westerhof
* @author Thomas Bruederli
* @url http://rc-calendar.lazlo.me
* @licence GNU GPL
* @copyright (c) 2010 Lazlo Westerhof - Netherlands
*
**/
CREATE TABLE `calendars` (
`calendar_id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`user_id` int(10) UNSIGNED NOT NULL DEFAULT '0',
`name` varchar(255) NOT NULL,
`color` varchar(8) NOT NULL,
PRIMARY KEY(`calendar_id`),
CONSTRAINT `fk_calendars_user_id` FOREIGN KEY (`user_id`)
REFERENCES `users`(`user_id`)
/*!40008
ON DELETE CASCADE
ON UPDATE CASCADE */
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
CREATE TABLE `events` (
`event_id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`calendar_id` int(11) UNSIGNED NOT NULL DEFAULT '0',
`recurrence_id` int(11) UNSIGNED NOT NULL DEFAULT '0',
`uid` varchar(255) NOT NULL DEFAULT '',
`created` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
`changed` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
`start` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
`end` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
`recurrence` varchar(255) DEFAULT NULL,
`title` varchar(255) NOT NULL,
`description` text NOT NULL,
`location` varchar(255) NOT NULL DEFAULT '',
`categories` varchar(255) NOT NULL DEFAULT '',
`all_day` tinyint(1) NOT NULL DEFAULT '0',
`free_busy` tinyint(1) NOT NULL DEFAULT '0',
`priority` tinyint(1) NOT NULL DEFAULT '1',
`alarms` varchar(255) DEFAULT NULL,
`attendees` text DEFAULT NULL,
PRIMARY KEY(`event_id`),
CONSTRAINT `fk_events_calendar_id` FOREIGN KEY (`calendar_id`)
REFERENCES `calendars`(`calendar_id`)
/*!40008
ON DELETE CASCADE
ON UPDATE CASCADE */
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
CREATE TABLE `attachments` (
`attachment_id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`event_id` int(11) UNSIGNED NOT NULL DEFAULT '0',
`filename` varchar(255) NOT NULL DEFAULT '',
`mimetype` varchar(255) NOT NULL DEFAULT '',
`size` int(11) NOT NULL DEFAULT '0',
`data` longtext NOT NULL DEFAULT '',
PRIMARY KEY(`attachment_id`),
CONSTRAINT `fk_attachments_event_id` FOREIGN KEY (`event_id`)
REFERENCES `events`(`event_id`)
/*!40008
ON DELETE CASCADE
ON UPDATE CASCADE */
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;

View file

@ -0,0 +1,40 @@
/**
* RoundCube Calendar
*
* Plugin to add a calendar to RoundCube.
*
* @version 0.2 BETA 2
* @author Lazlo Westerhof
* @author Albert Lee
* @url http://rc-calendar.lazlo.me
* @licence GNU GPL
* @copyright (c) 2010 Lazlo Westerhof - Netherlands
*
**/
CREATE SEQUENCE event_ids
INCREMENT BY 1
NO MAXVALUE
NO MINVALUE
CACHE 1;
CREATE TABLE events (
event_id integer DEFAULT nextval('event_ids'::regclass) NOT NULL,
user_id integer NOT NULL,
"start" timestamp without time zone DEFAULT now() NOT NULL,
"end" timestamp without time zone DEFAULT now() NOT NULL,
"title" character varying(255) NOT NULL,
"description" text NOT NULL,
"location" character varying(255) NOT NULL,
"categories" character varying(255) NOT NULL,
"all_day" smallint NOT NULL DEFAULT 0
);
CREATE INDEX events_event_id_idx ON events USING btree (event_id);
--
-- Constraints Table `events`
--
ALTER TABLE ONLY events
ADD CONSTRAINT events_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(user_id) ON UPDATE CASCADE ON DELETE CASCADE;

View file

@ -0,0 +1,58 @@
/**
* Roundcube Calendar
*
* Plugin to add a calendar to Roundcube.
*
* @version 0.3 beta
* @author Lazlo Westerhof
* @author Thomas Bruederli
* @author Albert Lee
* @url http://rc-calendar.lazlo.me
* @licence GNU GPL
* @copyright (c) 2010 Lazlo Westerhof - Netherlands
*
**/
CREATE TABLE calendars (
calendar_id integer NOT NULL PRIMARY KEY,
user_id integer NOT NULL default '0',
name varchar(255) NOT NULL default '',
color varchar(255) NOT NULL default '',
CONSTRAINT fk_calendars_user_id FOREIGN KEY (user_id)
REFERENCES users(user_id)
);
CREATE TABLE events (
event_id integer NOT NULL PRIMARY KEY,
calendar_id integer NOT NULL default '0',
recurrence_id integer NOT NULL default '0',
uid varchar(255) NOT NULL default '',
created datetime NOT NULL default '1000-01-01 00:00:00',
changed datetime NOT NULL default '1000-01-01 00:00:00',
start datetime NOT NULL default '1000-01-01 00:00:00',
end datetime NOT NULL default '1000-01-01 00:00:00',
recurrence varchar(255) default NULL,
title varchar(255) NOT NULL,
description text NOT NULL,
location varchar(255) NOT NULL default '',
categories varchar(255) NOT NULL default '',
all_day tinyint(1) NOT NULL default '0',
free_busy tinyint(1) NOT NULL default '0',
priority tinyint(1) NOT NULL default '1',
alarms varchar(255) default NULL,
attendees text default NULL,
CONSTRAINT fk_events_calendar_id FOREIGN KEY (calendar_id)
REFERENCES calendars(calendar_id)
);
CREATE TABLE attachments (
attachment_id integer NOT NULL PRIMARY KEY,
event_id integer NOT NULL default '0',
filename varchar(255) NOT NULL default '',
mimetype varchar(255) NOT NULL default '',
size integer NOT NULL default '0',
data text NOT NULL default '',
CONSTRAINT fk_attachment_event_id FOREIGN KEY (event_id)
REFERENCES events(event_id)
);

View file

@ -0,0 +1,176 @@
<?php
/*
+-------------------------------------------------------------------------+
| Kolab calendar storage class |
| |
| This program is free software; you can redistribute it and/or modify |
| it under the terms of the GNU General Public License version 2 |
| as published by the Free Software Foundation. |
| |
| PURPOSE: |
| Storage object for a single calendar folder on Kolab |
| |
+-------------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-------------------------------------------------------------------------+
*/
class Kolab_calendar
{
public $id;
public $ready = false;
public $readonly = true;
private $storage;
private $events;
private $id2uid;
private $imap_folder = 'INBOX/Calendar';
/**
* Default constructor
*/
public function __construct($imap_folder = null)
{
if ($imap_folder)
$this->imap_folder = $imap_folder;
// ID is derrived from folder name
$this->id = strtolower(asciiwords(strtr($this->imap_folder, '/.', '--')));
// fetch objects from the given IMAP folder
$this->storage = rcube_kolab::get_storage($this->imap_folder);
$this->ready = !PEAR::isError($this->storage);
}
/**
* Getter for a nice and human readable name for this calendar
* See http://wiki.kolab.org/UI-Concepts/Folder-Listing for reference
*
* @return string Name of this calendar
*/
public function get_name()
{
$dispname = preg_replace(array('!INBOX/Calendar/!', '!^INBOX/!', '!^shared/!', '!^user/([^/]+)/!'), array('','','','(\\1) '), $this->imap_folder);
return strlen($dispname) ? $dispname : $this->imap_folder;
}
/**
* Return color to display this calendar
*/
public function get_color()
{
// TODO: read color from backend (not yet supported)
return '0000cc';
}
/**
* Getter for a single event object
*/
public function get_event($id)
{
$this->_fetch_events();
return $this->events[$id];
}
/**
* @param integer Event's new start (unix timestamp)
* @param integer Event's new end (unix timestamp)
* @return array A list of event records
*/
public function list_events($start, $end)
{
$this->_fetch_events();
$events = array();
foreach ($this->events as $id => $event) {
// TODO: also list recurring events
if ($event['start'] >= $start && $event['end'] <= $end) {
$events[] = $event;
}
}
return $events;
}
/**
* Create a new event record
*
* @see Driver:new_event()
* @return mixed The created record ID on success, False on error
*/
public function insert_event($event)
{
return false;
}
/**
* Update a specific event record
*
* @see Driver:new_event()
* @return boolean True on success, False on error
*/
public function update_event($event)
{
return false;
}
/**
* Simply fetch all records and store them in private member vars
* We thereby rely on cahcing done by the Horde classes
*/
private function _fetch_events()
{
if (!isset($this->events)) {
$this->events = array();
foreach ((array)$this->storage->getObjects() as $record) {
$event = $this->_to_rcube_event($record);
$this->events[$event['id']] = $event;
}
}
}
/**
* Convert from Kolab_Format to internal representation
*/
private function _to_rcube_event($rec)
{
$start_time = date('H:i:s', $rec['start-date']);
$allday = $start_time == '00:00:00' && $start_time == date('H:i:s', $rec['end-date']);
return array(
'id' => $rec['uid'],
'uid' => $rec['uid'],
'title' => $rec['summary'],
'location' => $rec['location'],
'description' => $rec['body'],
'start' => $rec['start-date'],
'end' => $rec['end-date'],
'all_day' => $allday,
'categories' => $rec['categories'],
'free_busy' => $rec['show-time-as'],
'priority' => 1, // normal
'calendar' => $this->id,
);
}
/**
* Convert the given event record into a data structure that can be passed to Kolab_Storage backend for saving
* (opposite of self::_to_rcube_event())
*/
private function _from_rcube_eventt($event)
{
$object = array();
return $object;
}
}

View file

@ -0,0 +1,272 @@
<?php
/*
+-------------------------------------------------------------------------+
| Kolab driver for the Calendar Plugin |
| Version 0.3 beta |
| |
| This program is free software; you can redistribute it and/or modify |
| it under the terms of the GNU General Public License version 2 |
| as published by the Free Software Foundation. |
| |
| PURPOSE: |
| Kolab bindings for the calendar backend |
| |
+-------------------------------------------------------------------------+
| Author: Thomas Bruederli <roundcube@gmail.com> |
+-------------------------------------------------------------------------+
*/
require_once(dirname(__FILE__) . '/kolab_calendar.php');
class kolab_driver extends calendar_driver
{
// features this backend supports
public $attendees = false;
public $attachments = false;
private $rc;
private $cal;
private $calendars;
private $folders;
/**
* Default constructor
*/
public function __construct($cal)
{
$this->cal = $cal;
$this->rc = $cal->rc;
$this->_read_calendars();
}
/**
* Read available calendars from server
*/
private function _read_calendars()
{
// already read sources
if (isset($this->calendars))
return $this->calendars;
// get all folders that have "event" type
$folders = rcube_kolab::get_folders('event');
$this->folders = $this->calendars = array();
if (PEAR::isError($folders)) {
raise_error(array(
'code' => 600, 'type' => 'php',
'file' => __FILE__, 'line' => __LINE__,
'message' => "Failed to list calendar folders from Kolab server:" . $folders->getMessage()),
true, false);
}
else {
foreach ($folders as $c_folder) {
$calendar = new kolab_calendar($c_folder->name);
$this->folders[$calendar->id] = $calendar;
if ($calendar->ready) {
$this->calendars[$calendar->id] = array(
'id' => $calendar->id,
'name' => $calendar->get_name(),
'color' => $calendar->get_color(),
'readonly' => $c_folder->_owner != $_SESSION['username'],
);
}
}
}
return $this->calendars;
}
private function _get_storage($cid, $readonly = false)
{
if ($readonly)
return $this->folders[$cid];
else if (!$this->calendars[$cid]['readonly'])
return $this->folders[$cid];
return false;
}
/**
* Get a list of available calendars from this source
*/
public function list_calendars()
{
// attempt to create a default calendar for this user
if (empty($this->calendars)) {
if ($this->create_calendar(array('name' => 'Default', 'color' => 'cc0000')))
$this->_read_calendars();
}
return $this->calendars;
}
/**
* Create a new calendar assigned to the current user
*
* @param array Hash array with calendar properties
* name: Calendar name
* color: The color of the calendar
* @return mixed ID of the calendar on success, False on error
*/
public function create_calendar($prop)
{
return false;
}
/**
* Add a single event to the database
*
* @see Driver:new_event()
*/
public function new_event($event)
{
$cid = $event['calendar'] ? $event['calendar'] : reset(array_keys($this->calendars));
if ($storage = $this->_get_storage($cid))
return $storage->insert_event($event);
return false;
}
/**
* Update an event entry with the given data
*
* @see Driver:new_event()
* @return boolean True on success, False on error
*/
public function edit_event($event)
{
if ($storage = $this->_get_storage($event['calendar']))
return $storage->update_event($event);
return false;
}
/**
* Move a single event
*
* @see Driver:move_event()
* @return boolean True on success, False on error
*/
public function move_event($event)
{
if (($storage = $this->_get_storage($event['calendar'])) && ($ev = $storage->get_event($event['id'])))
return $storage->update_event($event + $ev);
return false;
}
/**
* Resize a single event
*
* @see Driver:resize_event()
* @return boolean True on success, False on error
*/
public function resize_event($event)
{
if (($storage = $this->_get_storage($event['calendar'])) && ($ev = $storage->get_event($event['id'])))
return $storage->update_event($event + $ev);
return false;
}
/**
* Remove a single event from the database
*
* @param array Hash array with event properties:
* id: Event identifier
* @return boolean True on success, False on error
*/
public function remove_event($event)
{
return false;
}
/**
* Get events from source.
*
* @param integer Event's new start (unix timestamp)
* @param integer Event's new end (unix timestamp)
* @param mixed List of calendar IDs to load events from (either as array or comma-separated string)
* @return array A list of event records
*/
public function load_events($start, $end, $calendars = null)
{
if ($calendars && is_string($calendars))
$calendars = explode(',', $calendars);
$events = array();
foreach ($this->calendars as $cid => $calendar) {
if ($calendars && !in_array($cid, $calendars))
continue;
$events = array_merge($this->folders[$cid]->list_events($start, $end));
}
return $events;
}
/**
* Search events using the given query
*
* @see Driver::search_events()
* @return array A list of event records
*/
public function search_events($start, $end, $query, $calendars = null)
{
return array();
}
/**
* Save an attachment related to the given event
*/
public function add_attachment($attachment, $event_id)
{
}
/**
* Remove a specific attachment from the given event
*/
public function remove_attachment($attachment, $event_id)
{
}
/**
* Create a new category
*/
public function add_category($name, $color)
{
}
/**
* Remove the given category
*/
public function remove_category($name)
{
}
/**
* Update/replace a category
*/
public function replace_category($oldname, $name, $color)
{
}
/**
* Fetch free/busy information from a person within the given range
*/
public function get_freebusy_list($email, $start, $end)
{
return array();
}
}

View file

@ -0,0 +1,82 @@
<?php
/*
+-------------------------------------------------------------------------+
| iCalendar functions for the Calendar Plugin |
| Version 0.3 beta |
| |
| This program is free software; you can redistribute it and/or modify |
| it under the terms of the GNU General Public License version 2 |
| as published by the Free Software Foundation. |
| |
| 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 General Public License for more details. |
| |
| You should have received a copy of the GNU General Public License along |
| with this program; if not, write to the Free Software Foundation, Inc., |
| 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
| |
+-------------------------------------------------------------------------+
| Author: Lazlo Westerhof <hello@lazlo.me> |
+-------------------------------------------------------------------------+
*/
class calendar_ical
{
private $rc;
private $driver;
function __construct($rc, $driver) {
$this->rc = $rc;
$this->driver = $driver;
}
/**
* Import events from iCalendar format
*
* @param array Associative events array
* @access public
*/
public function import($events) {
//TODO
// for ($events as $event)
// $this->backend->newEvent(...);
}
/**
* Export events to iCalendar format
*
* @param array Events as array
* @return string Events in iCalendar format (http://tools.ietf.org/html/rfc5545)
* @access public
*/
public function export($events) {
if (!empty($this->rc->user->ID)) {
$ical = "BEGIN:VCALENDAR\n";
$ical .= "VERSION:2.0\n";
$ical .= "PRODID:-//Roundcube Webmail//NONSGML Calendar//EN\n";
foreach ($events as $event) {
$ical .= "BEGIN:VEVENT\n";
$ical .= "DTSTART:" . date('Ymd\THis\Z', $event['start'] - date('Z')) . "\n";
$ical .= "DTEND:" . date('Ymd\THis\Z', $event['end'] - date('Z')) . "\n";
$ical .= "SUMMARY:" . $event['title'] . "\n";
$ical .= "DESCRIPTION:" . $event['description'] . "\n";
if (!empty($event['location'])) {
$ical .= "LOCATION:" . $event['location'] . "\n";
}
if(!empty($event['categories'])) {
$ical .= "CATEGORIES:" . strtoupper($event['categories']) . "\n";
}
if ($event['private']) {
$ical .= 'X-CALENDARSERVER-ACCESS:CONFIDENTIAL';
}
$ical .= 'TRANSP:' . ($event['free_busy'] == 'free' ? 'TRANSPARENT' : 'OPAQUE');
$ical .= "END:VEVENT\n";
}
$ical .= "END:VCALENDAR";
return $ical;
}
}
}

View file

@ -0,0 +1,394 @@
<?php
/*
+-------------------------------------------------------------------------+
| User Interface for the Calendar Plugin |
| Version 0.3 beta |
| |
| This program is free software; you can redistribute it and/or modify |
| it under the terms of the GNU General Public License version 2 |
| as published by the Free Software Foundation. |
| |
| 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 General Public License for more details. |
| |
| You should have received a copy of the GNU General Public License along |
| with this program; if not, write to the Free Software Foundation, Inc., |
| 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. |
| |
+-------------------------------------------------------------------------+
| Author: Lazlo Westerhof <hello@lazlo.me> |
+-------------------------------------------------------------------------+
*/
class calendar_ui
{
private $rc;
private $calendar;
function __construct($calendar)
{
$this->calendar = $calendar;
$this->rc = $calendar->rc;
}
/**
* Calendar UI initialization and requests handlers
*/
public function init()
{
// add taskbar button
$this->calendar->add_button(array(
'name' => 'calendar',
'class' => 'button-calendar',
'label' => 'calendar.calendar',
'href' => './?_task=calendar',
), 'taskbar');
}
/**
* Adds CSS stylesheets to the page header
*/
public function addCSS()
{
$skin = $this->rc->config->get('skin');
$this->calendar->include_stylesheet('skins/' . $skin . '/fullcalendar.css');
}
/**
* Adds JS files to the page header
*/
public function addJS()
{
$this->calendar->include_script('lib/js/fullcalendar.js');
$this->calendar->include_script('calendar.js');
}
/**
* Creates the Calendar toolbar
*/
public function toolbar()
{
$skin = $this->rc->config->get('skin');
$this->calendar->add_button(array(
'command' => 'plugin.export_events',
'href' => './?_task=calendar&amp;_action=plugin.export_events',
'title' => 'calendar.export',
'imagepas' => 'skins/' . $skin . '/images/export.png',
'imageact' => 'skins/' . $skin . '/images/export.png'),
'toolbar'
);
}
/**
*
*/
function calendar_css()
{
$categories = $this->rc->config->get('calendar_categories', array());
$css = "\n";
foreach ((array)$categories as $class => $color) {
$class = 'cat-' . asciiwords($class, true);
$css .= "." . $class . ",\n";
$css .= ".fc-event-" . $class . ",\n";
$css .= "." . $class . " a {\n";
$css .= "color: #" . $color . ";\n";
$css .= "border-color: #" . $color . ";\n";
$css .= "}\n";
}
$calendars = $this->calendar->driver->list_calendars();
foreach ((array)$calendars as $id => $prop) {
if (!$prop['color'])
continue;
$color = $prop['color'];
$class = 'cal-' . asciiwords($id, true);
$css .= "li." . $class . ", ";
$css .= "#eventshow ." . $class . " { ";
$css .= "color: #" . $color . " }\n";
$css .= ".fc-event-" . $class . ", ";
$css .= ".fc-event-" . $class . " .fc-event-inner, ";
$css .= ".fc-event-" . $class . " .fc-event-time {\n";
$css .= "background-color: #" . $color . ";\n";
$css .= "border-color: #" . $color . ";\n";
$css .= "}\n";
}
return html::tag('style', array('type' => 'text/css'), $css);
}
/**
*
*/
function calendar_list($attrib = array())
{
$calendars = $this->calendar->driver->list_calendars();
$li = '';
foreach ((array)$calendars as $id => $prop) {
unset($prop['user_id']);
$prop['attendees'] = $this->calendar->driver->attendees;
$prop['attachments'] = $this->calendar->driver->attachments;
$jsenv[$id] = $prop;
$html_id = html_identifier($id);
$li .= html::tag('li', array('id' => 'rcmlical' . $html_id, 'class' =>'cal-' . asciiwords($id, true)),
html::tag('input', array('type' => 'checkbox', 'name' => '_cal[]', 'value' => $id, 'checked' => true), '') . html::span(null, Q($prop['name'])));
}
$this->rc->output->set_env('calendars', $jsenv);
$this->rc->output->add_gui_object('folderlist', $attrib['id']);
unset($attrib['name']);
return html::tag('ul', $attrib, $li);
}
/**
* Render a HTML select box for calendar selection
*/
function calendar_select($attrib = array())
{
$attrib['name'] = 'calendar';
$select = new html_select($attrib);
foreach ((array)$this->calendar->driver->list_calendars() as $id => $prop) {
$select->add($prop['name'], $id);
}
return $select->show(null);
}
/**
* Render a HTML select box to select an event category
*/
function category_select($attrib = array())
{
$attrib['name'] = 'categories';
$select = new html_select($attrib);
$select->add('---', '');
foreach ((array)$this->calendar->driver->list_categories() as $cat => $color) {
$select->add($cat, $cat);
}
return $select->show(null);
}
/**
* Render a HTML select box for free/busy/out-of-office property
*/
function freebusy_select($attrib = array())
{
$attrib['name'] = 'freebusy';
$select = new html_select($attrib);
$select->add($this->calendar->gettext('free'), 'free');
$select->add($this->calendar->gettext('busy'), 'busy');
$select->add($this->calendar->gettext('outofoffice'), 'outofoffice');
return $select->show(null);
}
/**
* Render a HTML select for event priorities
*/
function priority_select($attrib = array())
{
$attrib['name'] = 'priority';
$select = new html_select($attrib);
$select->add($this->calendar->gettext('normal'), '1');
$select->add($this->calendar->gettext('low'), '0');
$select->add($this->calendar->gettext('high'), '2');
return $select->show(null);
}
/**
* Render HTML form for alarm configuration
*/
function alarm_select($attrib = array())
{
unset($attrib['name']);
$select_type = new html_select(array('name' => 'alarmtype[]', 'class' => 'edit-alarm-type'));
$select_type->add(
array($this->calendar->gettext('none'), $this->calendar->gettext('showmessage'), $this->calendar->gettext('byemail')),
array('','message','email'));
$input_value = new html_inputfield(array('name' => 'alarmvalue[]', 'class' => 'edit-alarm-value', 'size' => 3));
$input_date = new html_inputfield(array('name' => 'alarmdate[]', 'class' => 'edit-alarm-date', 'size' => 10));
$input_time = new html_inputfield(array('name' => 'alarmtime[]', 'class' => 'edit-alarm-time', 'size' => 6));
$select_offset = new html_select(array('name' => 'alarmoffset[]', 'class' => 'edit-alarm-offset'));
$select_offset->add(
array(
$this->calendar->gettext('minutesbefore'), $this->calendar->gettext('hoursbefore'), $this->calendar->gettext('daysbefore'),
$this->calendar->gettext('minutesafter'), $this->calendar->gettext('hoursafter'), $this->calendar->gettext('daysafter'),
$this->calendar->gettext('ondate'),
),
array('-m','-h','-d','+m','+h','+d','@'));
// TODO: pre-set with default values from user settings
$hidden = array('style' => 'display:none');
$html = html::span('edit-alarm-set',
$select_type->show('') . ' ' .
html::span(array('class' => 'edit-alarm-values', 'style' => 'display:none'),
$input_value->show(15) . ' ' .
$select_offset->show('-m') . ' ' .
$input_date->show('', $hidden) . ' ' .
$input_time->show('', $hidden)
)
);
// TODO: support adding more alarms
#$html .= html::a(array('href' => '#', 'id' => 'edit-alam-add', 'title' => $this->calendar->gettext('addalarm')),
# $attrib['addicon'] ? html::img(array('src' => $attrib['addicon'], 'alt' => 'add')) : '(+)');
return $html;
}
/**
* Generate the form for recurrence settings
*/
function recurrence_form($attrib = array())
{
switch ($attrib['part']) {
// frequency selector
case 'frequency':
$select = new html_select(array('name' => 'frequency', 'id' => 'edit-recurrence-frequency'));
$select->add($this->calendar->gettext('never'), '');
$select->add($this->calendar->gettext('daily'), 'DAILY');
$select->add($this->calendar->gettext('weekly'), 'WEEKLY');
$select->add($this->calendar->gettext('monthly'), 'MONTHLY');
$select->add($this->calendar->gettext('yearly'), 'YEARLY');
$html = html::label('edit-frequency', $this->calendar->gettext('frequency')) . $select->show('');
break;
// daily recurrence
case 'daily':
$select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-daily'));
$html = html::div($attrib, html::label(null, $this->calendar->gettext('every')) . $select->show(1) . html::span('label-after', $this->calendar->gettext('days')));
break;
// weekly recurrence form
case 'weekly':
$select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-weekly'));
$html = html::div($attrib, html::label(null, $this->calendar->gettext('every')) . $select->show(1) . html::span('label-after', $this->calendar->gettext('weeks')));
// weekday selection
$daymap = array('sun','mon','tue','wed','thu','fri','sat');
$checkbox = new html_checkbox(array('name' => 'byday', 'class' => 'edit-recurrence-weekly-byday'));
$first = $this->rc->config->get('calendar_first_day', 1);
for ($weekdays = '', $j = $first; $j <= $first+6; $j++) {
$d = $j % 7;
$weekdays .= html::label(array('class' => 'weekday'), $checkbox->show('', array('value' => strtoupper(substr($daymap[$d], 0, 2)))) . $this->calendar->gettext($daymap[$d])) . ' ';
}
$html .= html::div($attrib, html::label(null, $this->calendar->gettext('bydays')) . $weekdays);
break;
// monthly recurrence form
case 'monthly':
$select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-monthly'));
$html = html::div($attrib, html::label(null, $this->calendar->gettext('every')) . $select->show(1) . html::span('label-after', $this->calendar->gettext('months')));
// day of month selection
$checkbox = new html_checkbox(array('name' => 'bymonthday', 'class' => 'edit-recurrence-monthly-bymonthday'));
for ($monthdays = '', $d = 1; $d <= 31; $d++) {
$monthdays .= html::label(array('class' => 'monthday'), $checkbox->show('', array('value' => $d)) . $d);
$monthdays .= $d % 7 ? ' ' : html::br();
}
// rule selectors
$radio = new html_radiobutton(array('name' => 'repeatmode', 'class' => 'edit-recurrence-monthly-mode'));
$table = new html_table(array('cols' => 2, 'border' => 0, 'cellpadding' => 0, 'class' => 'formtable'));
$table->add('label topalign', html::label(null, $radio->show('BYMONTHDAY', array('value' => 'BYMONTHDAY')) . ' ' . $this->calendar->gettext('each')));
$table->add(null, $monthdays);
$table->add('label', html::label(null, $radio->show('', array('value' => 'BYDAY')) . ' ' . $this->calendar->gettext('onevery')));
$table->add(null, $this->rrule_selectors($attrib['part']));
$html .= html::div($attrib, $table->show());
break;
// annually recurrence form
case 'yearly':
$select = $this->interval_selector(array('name' => 'interval', 'class' => 'edit-recurrence-interval', 'id' => 'edit-recurrence-interval-yearly'));
$html = html::div($attrib, html::label(null, $this->calendar->gettext('every')) . $select->show(1) . html::span('label-after', $this->calendar->gettext('years')));
// month selector
$monthmap = array('','jan','feb','mar','apr','may','jun','jul','aug','sep','oct','nov','dec');
$checkbox = new html_checkbox(array('name' => 'bymonth', 'class' => 'edit-recurrence-yearly-bymonth'));
for ($months = '', $m = 1; $m <= 12; $m++) {
$months .= html::label(array('class' => 'month'), $checkbox->show('', array('value' => $m)) . $this->calendar->gettext($monthmap[$m]));
$months .= $m % 4 ? ' ' : html::br();
}
$html .= html::div($attrib + array('id' => 'edit-recurrence-yearly-bymonthblock'), $months);
// day rule selection
$html .= html::div($attrib, html::label(null, $this->calendar->gettext('onevery')) . $this->rrule_selectors($attrib['part'], '---'));
break;
// end of recurrence form
case 'until':
$radio = new html_radiobutton(array('name' => 'repeat', 'class' => 'edit-recurrence-until'));
$select = $this->interval_selector(array('name' => 'times', 'id' => 'edit-recurrence-repeat-times'));
$input = new html_inputfield(array('name' => 'untildate', 'id' => 'edit-recurrence-enddate', 'size' => "10"));
$table = new html_table(array('cols' => 2, 'border' => 0, 'cellpadding' => 0, 'class' => 'formtable'));
$table->add('label', $this->calendar->gettext('recurrencend'));
$table->add(null, html::label(null, $radio->show('', array('value' => '', 'id' => 'edit-recurrence-repeat-forever')) . ' ' .
$this->calendar->gettext('forever')));
$table->add('label', '');
$table->add(null, html::label(null, $radio->show('', array('value' => 'count', 'id' => 'edit-recurrence-repeat-count')) . ' ' .
$this->calendar->gettext(array(
'name' => 'forntimes',
'vars' => array('nr' => $select->show(1)))
)));
$table->add('label', '');
$table->add(null, $radio->show('', array('value' => 'until', 'id' => 'edit-recurrence-repeat-until')) . ' ' .
$this->calendar->gettext('until') . ' ' . $input->show(''));
$html = $table->show();
break;
}
return $html;
}
/**
* Input field for interval selection
*/
private function interval_selector($attrib)
{
$select = new html_select($attrib);
$select->add(range(1,30), range(1,30));
return $select;
}
/**
* Drop-down menus for recurrence rules like "each last sunday of"
*/
private function rrule_selectors($part, $noselect = null)
{
// rule selectors
$select_prefix = new html_select(array('name' => 'bydayprefix', 'id' => "edit-recurrence-$part-prefix"));
if ($noselect) $select_prefix->add($noselect, '');
$select_prefix->add(array(
$this->calendar->gettext('first'),
$this->calendar->gettext('second'),
$this->calendar->gettext('third'),
$this->calendar->gettext('fourth'),
$this->calendar->gettext('last')
),
array(1, 2, 3, 4, -1));
$select_wday = new html_select(array('name' => 'byday', 'id' => "edit-recurrence-$part-byday"));
if ($noselect) $select_wday->add($noselect, '');
$daymap = array('sunday','monday','tuesday','wednesday','thursday','friday','saturday');
$first = $this->rc->config->get('calendar_first_day', 1);
for ($j = $first; $j <= $first+6; $j++) {
$d = $j % 7;
$select_wday->add($this->calendar->gettext($daymap[$d]), strtoupper(substr($daymap[$d], 0, 2)));
}
if ($part == 'monthly')
$select_wday->add($this->calendar->gettext('dayofmonth'), '');
return $select_prefix->show() . '&nbsp;' . $select_wday->show();
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,38 @@
<?php
/**
* RoundCube Calendar
*
* Plugin to add a calendar to RoundCube.
*
* @version 0.2 BETA 2
* @author Lazlo Westerhof
* @url http://rc-calendar.lazlo.me
* @licence GNU GPL
* @copyright (c) 2010 Lazlo Westerhof - Netherlands
*
**/
$labels = array();
// config
$labels['default_view'] = 'Изглед';
$labels['time_format'] = 'Формат на часовете';
$labels['timeslots'] = 'Полета на час';
$labels['first_day'] = 'Първи ден от седмицата';
// calendar
$labels['calendar'] = 'Календар';
$labels['day'] = 'Ден';
$labels['week'] = 'Седмица';
$labels['month'] = 'Месец';
$labels['new_event'] = 'Добавяне на събитие';
$labels['edit_event'] = 'Промяна на събитие';
$labels['save'] = 'Запис';
$labels['remove'] = 'Изтриване';
$labels['cancel'] = 'Отказ';
$labels['title'] = 'Заглавие';
$labels['description'] = 'Описание';
$labels['all-day'] = 'цял ден';
$labels['export'] = 'Запази като ICS';
$labels['category'] = 'Категория';
?>

View file

@ -0,0 +1,38 @@
<?php
/**
* RoundCube Calendar
*
* Plugin to add a calendar to RoundCube.
*
* @version 0.2 BETA 2
* @author Lazlo Westerhof
* @url http://rc-calendar.lazlo.me
* @licence GNU GPL
* @copyright (c) 2010 Lazlo Westerhof - Netherlands
*
**/
$labels = array();
// config
$labels['default_view'] = 'Výchozí pohled';
$labels['time_format'] = 'Formát data';
$labels['timeslots'] = 'Slotů na hodinu';
$labels['first_day'] = 'První den v týdnu';
// calendar
$labels['calendar'] = 'Kalendář';
$labels['day'] = 'Den';
$labels['week'] = 'Týden';
$labels['month'] = 'Měsíc';
$labels['new_event'] = 'Nová událost';
$labels['edit_event'] = 'Editovat událost';
$labels['save'] = 'Uložit';
$labels['remove'] = 'Odstranit';
$labels['cancel'] = 'Storno';
$labels['title'] = 'Souhrn';
$labels['description'] = 'Popis';
$labels['all-day'] = 'celý den';
$labels['export'] = 'Exportovat do ICS';
$labels['category'] = 'Kategorie';
?>

View file

@ -0,0 +1,51 @@
<?php
/**
* RoundCube Calendar
*
* Plugin to add a calendar to RoundCube.
*
* @version 0.2 BETA 2
* @author Lazlo Westerhof
* @url http://rc-calendar.lazlo.me
* @licence GNU GPL
* @copyright (c) 2010 Lazlo Westerhof - Netherlands
*
**/
$labels = array();
// config
$labels['default_view'] = 'Ansicht';
$labels['time_format'] = 'Zeitformatierung';
$labels['timeslots'] = 'Zeitfenster pro Stunde';
$labels['first_day'] = 'Erster Wochentag';
// calendar
$labels['calendar'] = 'Kalender';
$labels['day'] = 'Tag';
$labels['week'] = 'Woche';
$labels['month'] = 'Monat';
$labels['new_event'] = 'Neuer Termin';
$labels['edit_event'] = 'Termin bearbeiten';
$labels['save'] = 'Speichern';
$labels['remove'] = 'Löschen';
$labels['cancel'] = 'Abbrechen';
$labels['edit'] = 'Bearbeiten';
$labels['title'] = 'Titel';
$labels['description'] = 'Beschreibung';
$labels['all-day'] = 'ganztägig';
$labels['export'] = 'Als ICS exportieren';
$labels['category'] = 'Kategorie';
$labels['location'] = 'Ort';
$labels['date'] = 'Datum';
$labels['start'] = 'Beginn';
$labels['end'] = 'Ende';
$labels['toggle_view'] = 'Ansicht wechseln';
$labels['generated'] = 'erstellt am';
$labels['selectdate'] = 'Datum auswählen';
$labels['prev_year'] = 'Vorheriges Jahr';
$labels['prev_month'] = 'Vorheriger Monat';
$labels['next_year'] = 'Nächstes Jahr';
$labels['next_month'] = 'Nächster Monat';
$labels['choose_date'] = 'Datum auswählen';
?>

View file

@ -0,0 +1,99 @@
<?php
$labels = array();
// preferences
$labels['default_view'] = 'Default view';
$labels['time_format'] = 'Time format';
$labels['timeslots'] = 'Timeslots per hour';
$labels['first_day'] = 'First weekday';
$labels['add_category'] = 'Add category';
$labels['remove_category'] = 'Remove category';
$labels['add_calendar'] = 'Add calendar';
$labels['remove_calendar'] = 'Remove calendar';
// calendar
$labels['calendar'] = 'Calendar';
$labels['calendars'] = 'Calendars';
$labels['category'] = 'Category';
$labels['categories'] = 'Categories';
$labels['createcalendar'] = 'Create new calendar';
$labels['day'] = 'Day';
$labels['week'] = 'Week';
$labels['month'] = 'Month';
$labels['new_event'] = 'New event';
$labels['edit_event'] = 'Edit event';
$labels['edit'] = 'Edit';
$labels['save'] = 'Save';
$labels['remove'] = 'Remove';
$labels['cancel'] = 'Cancel';
$labels['title'] = 'Summary';
$labels['description'] = 'Description';
$labels['all-day'] = 'all-day';
$labels['export'] = 'Export to ICS';
$labels['location'] = 'Location';
$labels['date'] = 'Date';
$labels['start'] = 'Start';
$labels['end'] = 'End';
$labels['choose_date'] = 'Choose date';
$labels['freebusy'] = 'Show as';
$labels['free'] = 'Free';
$labels['busy'] = 'Busy';
$labels['outofoffice'] = 'Out of Office';
$labels['priority'] = 'Priority';
$labels['alarms'] = 'Reminder';
$labels['generated'] = 'generated at';
$labels['selectdate'] = 'Select date';
$labels['prev_year'] = 'Previous year';
$labels['prev_month'] = 'Previous month';
$labels['next_year'] = 'Next year';
$labels['next_month'] = 'Next month';
$labels['choose_date'] = 'Choose date';
$labels['showmessage'] = 'Message';
$labels['byemail'] = 'E-mail';
$labels['ondate'] = 'on date';
$labels['minutesbefore'] = 'minutes before';
$labels['hoursbefore'] = 'hours before';
$labels['daysbefore'] = 'days before';
$labels['minutesafter'] = 'minutes after';
$labels['hoursafter'] = 'hours after';
$labels['daysafter'] = 'days after';
$labels['addalarm'] = 'add alarm';
$labels['tabsummary'] = 'Summary';
$labels['tabrecurrence'] = 'Recurrence';
$labels['tabattendees'] = 'Participants';
$labels['tabattachments'] = 'Attachments';
// messages
$labels['deleteventconfirm'] = "Do you really want to delete this event?";
$labels['errorsaving'] = "Failed to save changes";
$labels['operationfailed'] = "The requested operation failed";
// recurrence form
$labels['frequency'] = 'Repeat';
$labels['never'] = 'never';
$labels['daily'] = 'daily';
$labels['weekly'] = 'weekly';
$labels['monthly'] = 'monthly';
$labels['yearly'] = 'annually';
$labels['every'] = 'Every';
$labels['days'] = 'day(s)';
$labels['weeks'] = 'week(s)';
$labels['months'] = 'month(s)';
$labels['years'] = 'year(s) in:';
$labels['bydays'] = 'On';
$labels['until'] = 'the';
$labels['each'] = 'Each';
$labels['onevery'] = 'On every';
$labels['forever'] = 'forever';
$labels['recurrencend'] = 'Until';
$labels['forntimes'] = 'for $nr time(s)';
$labels['first'] = 'first';
$labels['second'] = 'second';
$labels['third'] = 'third';
$labels['fourth'] = 'fourth';
$labels['last'] = 'last';
$labels['dayofmonth'] = 'Day of month';
?>

View file

@ -0,0 +1,39 @@
<?php
/**
* RoundCube Calendar
*
* Plugin to add a calendar to RoundCube.
*
* @version 0.2 BETA 3
* @author Lazlo Westerhof
* @author David Iribas
* @url http://rc-calendar.lazlo.me
* @licence GNU GPL
* @copyright (c) 2010 Lazlo Westerhof - Netherlands
*
**/
$labels = array();
// config
$labels['default_view'] = 'Vista por defecto';
$labels['time_format'] = 'Formato hora';
$labels['timeslots'] = 'Periodos de hora';
$labels['first_day'] = 'Primer dia';
// calendar
$labels['calendar'] = 'Calendario';
$labels['day'] = 'Dia';
$labels['week'] = 'Semana';
$labels['month'] = 'Mes';
$labels['new_event'] = 'Nuevo evento';
$labels['edit_event'] = 'Editar evento';
$labels['save'] = 'Guardar';
$labels['remove'] = 'Borrar';
$labels['cancel'] = 'Cancelar';
$labels['title'] = 'Resumen';
$labels['description'] = 'Descripcion';
$labels['all-day'] = 'dia-completo';
$labels['export'] = 'Exportar a ICS';
$labels['category'] = 'Categoria';
?>

View file

@ -0,0 +1,38 @@
<?php
/**
* RoundCube Calendar
*
* Plugin to add a calendar to RoundCube.
*
* @version 0.2 BETA 2
* @author Lazlo Westerhof
* @url http://rc-calendar.lazlo.me
* @licence GNU GPL
* @copyright (c) 2010 Lazlo Westerhof - Netherlands
*
**/
$labels = array();
// config
$labels['default_view'] = 'Vue par défaut';
$labels['time_format'] = 'Format de l\'heure';
$labels['timeslots'] = 'Intervalle de temps par heure';
$labels['first_day'] = 'Premier jour de la semaine';
// calendar
$labels['calendar'] = 'Calendrier';
$labels['day'] = 'Jour';
$labels['week'] = 'Semaine';
$labels['month'] = 'Mois';
$labels['new_event'] = 'Nouvel évènement';
$labels['edit_event'] = 'Editer l\'évènement';
$labels['save'] = 'Enregistrer';
$labels['remove'] = 'Supprimer';
$labels['cancel'] = 'Annuler';
$labels['title'] = 'Sommaire';
$labels['description'] = 'Description';
$labels['all-day'] = 'Toute la journée';
$labels['export'] = 'Exporter en format ICS';
$labels['category'] = 'Categorie';
?>

View file

@ -0,0 +1,38 @@
<?php
/**
* RoundCube Calendar
*
* Plugin to add a calendar to RoundCube.
*
* @version 0.2 BETA 2
* @author Lazlo Westerhof
* @url http://rc-calendar.lazlo.me
* @licence GNU GPL
* @copyright (c) 2010 Lazlo Westerhof - Netherlands
*
**/
$labels = array();
// config
$labels['default_view'] = 'Alap nézet';
$labels['time_format'] = 'Idõ formátum';
$labels['timeslots'] = 'Idõrés óránként';
$labels['first_day'] = 'A hét elsõ napja';
// calendar
$labels['calendar'] = 'Naptár';
$labels['day'] = 'Nap';
$labels['week'] = 'Hét';
$labels['month'] = 'Hónap';
$labels['new_event'] = 'Új bejegyzés';
$labels['edit_event'] = 'Bejegyzés szerkesztése';
$labels['save'] = 'Mentés';
$labels['remove'] = 'Törlés';
$labels['cancel'] = 'Mégse';
$labels['title'] = 'Tárgy';
$labels['description'] = 'Leírás';
$labels['all-day'] = 'Egész nap';
$labels['export'] = 'Exportálás ICS formátumba';
$labels['category'] = 'Kategória';
?>

View file

@ -0,0 +1,38 @@
<?php
/**
* RoundCube Calendar
*
* Plugin to add a calendar to RoundCube.
*
* @version 0.2 BETA 2
* @author Lazlo Westerhof
* @url http://rc-calendar.lazlo.me
* @licence GNU GPL
* @copyright (c) 2010 Lazlo Westerhof - Netherlands
*
**/
$labels = array();
// config
$labels['default_view'] = 'Visualizzazine di default';
$labels['time_format'] = 'Formato ora';
$labels['timeslots'] = 'Timeslots per ora';
$labels['first_day'] = 'Inizio settimana';
// calendar
$labels['calendar'] = 'Calendario';
$labels['day'] = 'Giorno';
$labels['week'] = 'Settimana';
$labels['month'] = 'Mese';
$labels['new_event'] = 'Nuovo evento';
$labels['edit_event'] = 'Modifica evento';
$labels['save'] = 'Salva';
$labels['remove'] = 'Elimina';
$labels['cancel'] = 'Annulla';
$labels['title'] = 'Oggetto';
$labels['description'] = 'Descrizione';
$labels['all-day'] = 'Giorno intero';
$labels['export'] = 'Esporta in ICS';
$labels['category'] = 'Categoria';
?>

View file

@ -0,0 +1,39 @@
<?php
/**
* RoundCube Calendar
*
* Plugin to add a calendar to RoundCube.
*
* @version 0.2 BETA 2
* @author Lazlo Westerhof
* @url http://rc-calendar.lazlo.me
* @licence GNU GPL
* @copyright (c) 2010 Lazlo Westerhof - Netherlands
*
**/
$labels = array();
// config
$labels['default_view'] = 'Standaard agenda';
$labels['time_format'] = 'Tijdsformaat';
$labels['timeslots'] = 'Timeslots per uur';
$labels['first_day'] = 'Eerste weekdag';
// calendar
$labels['calendar'] = 'Agenda';
$labels['day'] = 'Dag';
$labels['week'] = 'Week';
$labels['month'] = 'Maand';
$labels['new_event'] = 'Nieuw evenement';
$labels['edit_event'] = 'Wijzig evenement';
$labels['save'] = 'Bewaar';
$labels['remove'] = 'Verwijder';
$labels['cancel'] = 'Terug';
$labels['title'] = 'Samenvatting';
$labels['description'] = 'Beschrijving';
$labels['all-day'] = 'hele dag';
$labels['export'] = 'Exporteer naar ICS';
$labels['category'] = 'Categorie';
$labels['location'] = 'Locatie';
?>

View file

@ -0,0 +1,39 @@
<?php
/**
* RoundCube Calendar
*
* Plugin to add a calendar to RoundCube.
*
* @version 0.2 BETA 2
* @author Lazlo Westerhof
* @url http://rc-calendar.lazlo.me
* @licence GNU GPL
* @copyright (c) 2010 Lazlo Westerhof - Netherlands
*
**/
$labels = array();
// config
$labels['default_view'] = 'Domyślny widok';
$labels['time_format'] = 'Format czasu';
$labels['timeslots'] = 'Przedzialy czasowe na godzinę';
$labels['first_day'] = 'Pierwszy dzień tygodnia';
// calendar
$labels['calendar'] = 'Kalendarz';
$labels['day'] = 'Dzień';
$labels['week'] = 'Tydzień';
$labels['month'] = 'Miesiąc';
$labels['new_event'] = 'Nowe zdarzenie';
$labels['edit_event'] = 'Edycja zdarzenia';
$labels['save'] = 'Zapisz';
$labels['remove'] = 'Usuń';
$labels['cancel'] = 'Anuluj';
$labels['title'] = 'Temat';
$labels['description'] = 'Opis';
$labels['all-day'] = 'cały dzień';
$labels['export'] = 'Eksport do ICS';
$labels['category'] = 'Kategoria';
$labels['location'] = 'Lokalizacja';
?>

View file

@ -0,0 +1,38 @@
<?php
/**
* RoundCube Calendar
*
* Plugin to add a calendar to RoundCube.
*
* @version 0.2 BETA 2
* @author Lazlo Westerhof
* @url http://rc-calendar.lazlo.me
* @licence GNU GPL
* @copyright (c) 2010 Lazlo Westerhof - Netherlands
*
**/
$labels = array();
// config
$labels['default_view'] = 'Visualização padrão';
$labels['time_format'] = 'Formato da hora';
$labels['timeslots'] = 'Caixas de tempo por hora';
$labels['first_day'] = 'Primeiro dia da semana';
// calendar
$labels['calendar'] = 'Calendário';
$labels['day'] = 'Dia';
$labels['week'] = 'Semana';
$labels['month'] = 'Mês';
$labels['new_event'] = 'Novo evento';
$labels['edit_event'] = 'Editar evento';
$labels['save'] = 'Gravar';
$labels['remove'] = 'Remover';
$labels['cancel'] = 'Cancelar';
$labels['title'] = 'Sumário';
$labels['description'] = 'Descrição';
$labels['all-day'] = 'dia-a-dia';
$labels['export'] = 'Exportar para ICS';
$labels['category'] = 'Categoria';
?>

View file

@ -0,0 +1,32 @@
<?php
/**
* RoundCube Calendar
*
* Plugin to add a calendar to RoundCube.
*
* @version 0.2 BETA 2
* @author Lazlo Westerhof
* @url http://rc-calendar.lazlo.me
* @licence GNU GPL
* @copyright (c) 2010 Lazlo Westerhof - Netherlands
*
**/
$labels = array();
// config
$labels['time_format'] = 'Формат времени';
$labels['timeslots'] = 'Промежутков в час';
$labels['first_day'] = 'Первый день недели';
// calendar
$labels['calendar'] = 'Календарь';
$labels['day'] = 'День';
$labels['week'] = 'Неделя';
$labels['month'] = 'Месяц';
$labels['new_event'] = 'Новое событие';
$labels['edit_event'] = 'Изменить событие';
$labels['save'] = 'Сохранить';
$labels['remove'] = 'Удалить';
$labels['cancel'] = 'Завершить';
?>

View file

@ -0,0 +1,5 @@
Icons by Fugue Icons <http://p.yusukekamiyamane.com/>
Copyright (C) 2010 Yusuke Kamiyamane. All rights reserved.
The icons are licensed under a Creative Commons Attribution
3.0 license. <http://creativecommons.org/licenses/by/3.0/>

View file

@ -0,0 +1,392 @@
/*** Style for Calendar plugin ***/
body.calendarmain {
overflow: hidden;
}
#taskbar a.button-calendar {
background: url('images/calendar.png') 0px 1px no-repeat;
}
#main {
position: absolute;
clear: both;
top: 85px;
left: 0;
right: 0;
bottom: 10px;
}
#sidebar {
position: absolute;
top: 37px;
left: 10px;
bottom: 0;
width: 230px;
}
#datepicker {
width: 100%;
}
#datepicker .ui-datepicker {
width: 97% !important;
-moz-box-shadow: none;
-webkit-box-shadow: none;
}
#datepicker .ui-priority-secondary {
opacity: 0.4;
}
#sidebartoggle {
position: absolute;
left: 246px;
width: 8px;
top: 37px;
bottom: 0;
background: url('images/toggle.gif') 0 48% no-repeat transparent;
cursor: pointer;
}
div.sidebarclosed {
background-position: -8px 48% !important;
}
#sidebartoggle:hover {
background-color: #ddd;
}
#calendar {
position: absolute;
top: 0;
left: 260px;
right: 10px;
bottom: 0;
}
#print {
width: 680px;
}
pre {
font-family: "Lucida Grande", Verdana, Arial, Helvetica, sans-serif;
}
#calendars {
position: absolute;
top: 230px;
left: 0;
bottom: 0;
right: 0;
background-color: #F9F9F9;
border: 1px solid #999999;
overflow: hidden;
}
#calendarslist {
list-style: none;
margin: 0;
padding: 0;
}
#calendarslist li {
margin: 0;
padding: 2px;
display: block;
background: #fff;
border-bottom: 1px solid #EBEBEB;
}
#calendarslist li label {
display: block;
}
#calendarslist li span {
cursor: default;
}
#calendarslist li input {
margin-right: 5px;
}
#calendarslist li.selected {
background-color: #ccc;
border-bottom: 1px solid #999;
}
#calendarslist li.selected span {
font-weight: bold;
}
#agendalist {
width: 100%;
margin: 0 auto;
margin-top: 60px;
border: 1px solid #C1DAD7;
display: none;
}
#agendalist table {
width: 100%;
}
#agendalist td, th {
border-right: 1px solid #C1DAD7;
border-bottom: 1px solid #C1DAD7;
background: #fff;
padding: 6px 6px 6px 12px;
}
#agendalist tr {
vertical-align: top;
}
#agendalist th {
font-weight: bold;
}
#calendartoolbar {
position: absolute;
top: 45px;
left: 260px;
height: 35px;
}
#calendartoolbar a {
padding-right: 10px;
}
#calendartoolbar a.button,
#calendartoolbar a.buttonPas {
display: block;
float: left;
width: 32px;
height: 32px;
padding: 0;
margin-right: 10px;
overflow: hidden;
background: url('images/toolbar.png') 0 0 no-repeat transparent;
opacity: 0.99; /* this is needed to make buttons appear correctly in Chrome */
}
#calendartoolbar a.buttonPas {
opacity: 0.35;
}
#calendartoolbar a.addeventSel {
background-position: 0 -32px;
}
#calendartoolbar a.delete {
background-position: -32px 0;
}
#calendartoolbar a.deleteSel {
background-position: -32px -32px;
}
#calendartoolbar a.print {
background-position: -64px 0;
}
#calendartoolbar a.printSel {
background-position: -64px -32px;
}
#calendartoolbar a.export {
background-position: -128px 0;
}
#calendartoolbar a.exportSel {
background-position: -128px -32px;
}
#quicksearchbar {
right: 10px;
}
#eventshow,
#eventedit {
display: none;
}
#user {
position: absolute;
top: 10px;
right: 100px;
left: 100px;
text-align: center;
}
/* jQuery UI overrides */
#eventshow h1 {
font-size: 20px;
margin: 0.1em 0 0.4em 0;
}
#eventshow label,
#eventshow h5.label {
font-weight: normal;
font-size: 0.9em;
color: #999;
margin: 0 0 0.2em 0;
}
#eventshow div.event-line {
margin-top: 0.1em;
margin-bottom: 0.3em;
}
#eventedit {
padding: 0.5em 0.1em;
}
#eventedit input.text,
#eventedit textarea {
width: 97%;
}
#eventtabs {
position: relative;
padding: 0;
border: 0;
border-radius: 0;
}
#eventshow div.event-section,
#eventtabs div.event-section {
margin-top: 0.2em;
margin-bottom: 0.8em;
}
#eventtabs .tabsbar {
position: absolute;
top: 0;
}
#eventtabs .ui-tabs-panel {
padding: 1em 0.8em;
border: 1px solid #aaa;
border-width: 0 1px 1px 1px;
}
#eventtabs .ui-tabs-nav {
background: none;
padding: 0;
border-width: 0 0 1px 0;
border-radius: 0;
}
#eventtabs .border-after {
padding-bottom: 0.6em;
margin-bottom: 0.6em;
border-bottom: 1px solid #999;
}
#eventshow label,
#eventedit label {
display: inline-block;
min-width: 7em;
padding-right: 0.5em;
}
#eventedit .formtable td.label {
min-width: 6em;
}
td.topalign {
vertical-align: top;
}
#eventedit label.weekday,
#eventedit label.monthday {
min-width: 3em;
}
#eventedit label.month {
min-width: 5em;
}
#edit-recurrence-yearly-bymonthblock {
margin-left: 7.5em;
}
#eventedit .recurrence-form {
display: none;
}
#eventedit .formtable td {
padding: 0.2em 0;
}
a.dropdown-link {
color: #CC0000;
font-size: 12px;
text-decoration: none;
}
a.dropdown-link:after {
content: ' ▼';
font-size: 11px;
color: #666;
}
#eventedit .ui-tabs-panel {
min-height: 20em;
}
.ui-dialog-buttonset a.dropdown-link {
margin-right: 1em;
}
.ui-datepicker-calendar .ui-datepicker-today .ui-state-default {
border-color: #cccccc;
background: #ffffcc;
color: #000;
}
.ui-datepicker-calendar .ui-datepicker-week-col {
text-align: right;
padding-right: 0.6em;
}
.ui-autocomplete {
max-height: 160px;
overflow-y: auto;
overflow-x: hidden;
}
* html .ui-autocomplete {
height: 160px;
}
/* fullcalendar style overrides */
.fc-event-title {
font-weight: bold;
}
.fc-event-cateories {
font-style:italic;
}
.fc-agenda-slots td div {
height: 22px;
}
.fc-mon, .fc-tue, .fc-wed, .fc-thu, .fc-fri {
background-color: #fdfdfd;
}
.fc-widget-header {
background-color: #fff;
}
/* Settings section */
fieldset #calendarcategories div {
margin-bottom: 0.3em;
}

View file

@ -0,0 +1,618 @@
/*
* FullCalendar v1.5.1 Stylesheet
*
* Copyright (c) 2011 Adam Shaw
* Dual licensed under the MIT and GPL licenses, located in
* MIT-LICENSE.txt and GPL-LICENSE.txt respectively.
*
* Date: Sat Apr 9 14:09:51 2011 -0700
*
*/
.fc {
direction: ltr;
text-align: left;
}
.fc table {
border-collapse: collapse;
border-spacing: 0;
}
html .fc,
.fc table {
font-size: 1em;
}
.fc td,
.fc th {
padding: 0;
vertical-align: top;
}
/* Header
------------------------------------------------------------------------*/
.fc-header td {
white-space: nowrap;
}
.fc-header-left {
width: 25%;
text-align: left;
}
.fc-header-center {
text-align: center;
}
.fc-header-right {
width: 25%;
text-align: right;
}
.fc-header-title {
display: inline-block;
vertical-align: top;
}
.fc-header-title h2 {
margin-top: 0;
white-space: nowrap;
}
.fc .fc-header-space {
padding-left: 10px;
}
.fc-header .fc-button {
margin-bottom: 1em;
vertical-align: top;
}
/* buttons edges butting together */
.fc-header .fc-button {
margin-right: -1px;
}
.fc-header .fc-corner-right {
margin-right: 1px; /* back to normal */
}
.fc-header .ui-corner-right {
margin-right: 0; /* back to normal */
}
/* button layering (for border precedence) */
.fc-header .fc-state-hover,
.fc-header .ui-state-hover {
z-index: 2;
}
.fc-header .fc-state-down {
z-index: 3;
}
.fc-header .fc-state-active,
.fc-header .ui-state-active {
z-index: 4;
}
/* Content
------------------------------------------------------------------------*/
.fc-content {
clear: both;
}
.fc-view {
width: 100%; /* needed for view switching (when view is absolute) */
overflow: hidden;
}
/* Cell Styles
------------------------------------------------------------------------*/
.fc-widget-header, /* <th>, usually */
.fc-widget-content { /* <td>, usually */
border: 1px solid #ccc;
}
.fc-state-highlight { /* <td> today cell */ /* TODO: add .fc-today to <th> */
background: #ffc;
}
.fc-cell-overlay { /* semi-transparent rectangle while dragging */
background: #9cf;
opacity: .2;
filter: alpha(opacity=20); /* for IE */
}
/* Buttons
------------------------------------------------------------------------*/
.fc-button {
position: relative;
display: inline-block;
cursor: pointer;
}
.fc-state-default { /* non-theme */
border-style: solid;
border-width: 1px 0;
}
.fc-button-inner {
position: relative;
float: left;
overflow: hidden;
}
.fc-state-default .fc-button-inner { /* non-theme */
border-style: solid;
border-width: 0 1px;
}
.fc-button-content {
position: relative;
float: left;
height: 1.9em;
line-height: 1.9em;
padding: 0 .6em;
white-space: nowrap;
}
/* icon (for jquery ui) */
.fc-button-content .fc-icon-wrap {
position: relative;
float: left;
top: 50%;
}
.fc-button-content .ui-icon {
position: relative;
float: left;
margin-top: -50%;
*margin-top: 0;
*top: -50%;
}
/* gloss effect */
.fc-state-default .fc-button-effect {
position: absolute;
top: 50%;
left: 0;
}
.fc-state-default .fc-button-effect span {
position: absolute;
top: -100px;
left: 0;
width: 500px;
height: 100px;
border-width: 100px 0 0 1px;
border-style: solid;
border-color: #fff;
background: #444;
opacity: .09;
filter: alpha(opacity=9);
}
/* button states (determines colors) */
.fc-state-default,
.fc-state-default .fc-button-inner {
border-style: solid;
border-color: #ccc #bbb #aaa;
background: #F3F3F3;
color: #000;
}
.fc-state-hover,
.fc-state-hover .fc-button-inner {
border-color: #999;
}
.fc-state-down,
.fc-state-down .fc-button-inner {
border-color: #555;
background: #777;
}
.fc-state-active,
.fc-state-active .fc-button-inner {
border-color: #555;
background: #777;
color: #fff;
}
.fc-state-disabled,
.fc-state-disabled .fc-button-inner {
color: #999;
border-color: #ddd;
}
.fc-state-disabled {
cursor: default;
}
.fc-state-disabled .fc-button-effect {
display: none;
}
/* Global Event Styles
------------------------------------------------------------------------*/
.fc-event {
border-style: solid;
border-width: 0;
font-size: .85em;
cursor: default;
}
a.fc-event,
.fc-event-draggable {
cursor: pointer;
}
a.fc-event {
text-decoration: none;
}
.fc-rtl .fc-event {
text-align: right;
}
.fc-event-skin {
border-color: #36c; /* default BORDER color */
background-color: #36c; /* default BACKGROUND color */
color: #fff; /* default TEXT color */
}
.fc-event-inner {
position: relative;
width: 100%;
height: 100%;
border-style: solid;
border-width: 0;
overflow: hidden;
}
.fc-event-time,
.fc-event-title {
padding: 0 1px;
}
.fc .ui-resizable-handle { /*** TODO: don't use ui-resizable anymore, change class ***/
display: block;
position: absolute;
z-index: 99999;
overflow: hidden; /* hacky spaces (IE6/7) */
font-size: 300%; /* */
line-height: 50%; /* */
}
/* Horizontal Events
------------------------------------------------------------------------*/
.fc-event-hori {
border-width: 1px 0;
margin-bottom: 1px;
}
/* resizable */
.fc-event-hori .ui-resizable-e {
top: 0 !important; /* importants override pre jquery ui 1.7 styles */
right: -3px !important;
width: 7px !important;
height: 100% !important;
cursor: e-resize;
}
.fc-event-hori .ui-resizable-w {
top: 0 !important;
left: -3px !important;
width: 7px !important;
height: 100% !important;
cursor: w-resize;
}
.fc-event-hori .ui-resizable-handle {
_padding-bottom: 14px; /* IE6 had 0 height */
}
/* Fake Rounded Corners (for buttons and events)
------------------------------------------------------------*/
.fc-corner-left {
margin-left: 1px;
}
.fc-corner-left .fc-button-inner,
.fc-corner-left .fc-event-inner {
margin-left: -1px;
}
.fc-corner-right {
margin-right: 1px;
}
.fc-corner-right .fc-button-inner,
.fc-corner-right .fc-event-inner {
margin-right: -1px;
}
.fc-corner-top {
margin-top: 1px;
}
.fc-corner-top .fc-event-inner {
margin-top: -1px;
}
.fc-corner-bottom {
margin-bottom: 1px;
}
.fc-corner-bottom .fc-event-inner {
margin-bottom: -1px;
}
/* Fake Rounded Corners SPECIFICALLY FOR EVENTS
-----------------------------------------------------------------*/
.fc-corner-left .fc-event-inner {
border-left-width: 1px;
}
.fc-corner-right .fc-event-inner {
border-right-width: 1px;
}
.fc-corner-top .fc-event-inner {
border-top-width: 1px;
}
.fc-corner-bottom .fc-event-inner {
border-bottom-width: 1px;
}
/* Reusable Separate-border Table
------------------------------------------------------------*/
table.fc-border-separate {
border-collapse: separate;
}
.fc-border-separate th,
.fc-border-separate td {
border-width: 1px 0 0 1px;
}
.fc-border-separate th.fc-last,
.fc-border-separate td.fc-last {
border-right-width: 1px;
}
.fc-border-separate tr.fc-last th,
.fc-border-separate tr.fc-last td {
border-bottom-width: 1px;
}
.fc-border-separate tbody tr.fc-first td,
.fc-border-separate tbody tr.fc-first th {
border-top-width: 0;
}
/* Month View, Basic Week View, Basic Day View
------------------------------------------------------------------------*/
.fc-grid th {
text-align: center;
}
.fc-grid .fc-day-number {
float: right;
padding: 0 2px;
}
.fc-grid .fc-other-month .fc-day-number {
opacity: 0.3;
filter: alpha(opacity=30); /* for IE */
/* opacity with small font can sometimes look too faded
might want to set the 'color' property instead
making day-numbers bold also fixes the problem */
}
.fc-grid .fc-day-content {
clear: both;
padding: 2px 2px 1px; /* distance between events and day edges */
}
/* event styles */
.fc-grid .fc-event-time {
font-weight: bold;
}
/* right-to-left */
.fc-rtl .fc-grid .fc-day-number {
float: left;
}
.fc-rtl .fc-grid .fc-event-time {
float: right;
}
/* Agenda Week View, Agenda Day View
------------------------------------------------------------------------*/
.fc-agenda table {
border-collapse: separate;
}
.fc-agenda-days th {
text-align: center;
}
.fc-agenda .fc-agenda-axis {
width: 50px;
padding: 0 4px;
vertical-align: middle;
text-align: right;
white-space: nowrap;
font-weight: normal;
}
.fc-agenda .fc-day-content {
padding: 2px 2px 1px;
}
/* make axis border take precedence */
.fc-agenda-days .fc-agenda-axis {
border-right-width: 1px;
}
.fc-agenda-days .fc-col0 {
border-left-width: 0;
}
/* all-day area */
.fc-agenda-allday th {
border-width: 0 1px;
}
.fc-agenda-allday .fc-day-content {
min-height: 34px; /* TODO: doesnt work well in quirksmode */
_height: 34px;
}
/* divider (between all-day and slots) */
.fc-agenda-divider-inner {
height: 2px;
overflow: hidden;
}
.fc-widget-header .fc-agenda-divider-inner {
background: #eee;
}
/* slot rows */
.fc-agenda-slots th {
border-width: 1px 1px 0;
}
.fc-agenda-slots td {
border-width: 1px 0 0;
background: none;
}
.fc-agenda-slots td div {
height: 20px;
}
.fc-agenda-slots tr.fc-slot0 th,
.fc-agenda-slots tr.fc-slot0 td {
border-top-width: 0;
}
.fc-agenda-slots tr.fc-minor th,
.fc-agenda-slots tr.fc-minor td {
border-top-style: dotted;
}
.fc-agenda-slots tr.fc-minor th.ui-widget-header {
*border-top-style: solid; /* doesn't work with background in IE6/7 */
}
/* Vertical Events
------------------------------------------------------------------------*/
.fc-event-vert {
border-width: 0 1px;
}
.fc-event-vert .fc-event-head,
.fc-event-vert .fc-event-content {
position: relative;
z-index: 2;
width: 100%;
overflow: hidden;
}
.fc-event-vert .fc-event-time {
white-space: nowrap;
font-size: 10px;
}
.fc-event-vert .fc-event-bg { /* makes the event lighter w/ a semi-transparent overlay */
position: absolute;
z-index: 1;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: #fff;
opacity: .3;
filter: alpha(opacity=30);
}
.fc .ui-draggable-dragging .fc-event-bg, /* TODO: something nicer like .fc-opacity */
.fc-select-helper .fc-event-bg {
display: none\9; /* for IE6/7/8. nested opacity filters while dragging don't work */
}
/* resizable */
.fc-event-vert .ui-resizable-s {
bottom: 0 !important; /* importants override pre jquery ui 1.7 styles */
width: 100% !important;
height: 8px !important;
overflow: hidden !important;
line-height: 8px !important;
font-size: 11px !important;
font-family: monospace;
text-align: center;
cursor: s-resize;
}
.fc-agenda .ui-resizable-resizing { /* TODO: better selector */
_overflow: hidden;
}

View file

@ -0,0 +1,61 @@
/*
* FullCalendar v1.5.1 Print Stylesheet
*
* Include this stylesheet on your page to get a more printer-friendly calendar.
* When including this stylesheet, use the media='print' attribute of the <link> tag.
* Make sure to include this stylesheet IN ADDITION to the regular fullcalendar.css.
*
* Copyright (c) 2011 Adam Shaw
* Dual licensed under the MIT and GPL licenses, located in
* MIT-LICENSE.txt and GPL-LICENSE.txt respectively.
*
* Date: Sat Apr 9 14:09:51 2011 -0700
*
*/
/* Events
-----------------------------------------------------*/
.fc-event-skin {
background: none !important;
color: #000 !important;
}
/* horizontal events */
.fc-event-hori {
border-width: 0 0 1px 0 !important;
border-bottom-style: dotted !important;
border-bottom-color: #000 !important;
padding: 1px 0 0 0 !important;
}
.fc-event-hori .fc-event-inner {
border-width: 0 !important;
padding: 0 1px !important;
}
/* vertical events */
.fc-event-vert {
border-width: 0 0 0 1px !important;
border-left-style: dotted !important;
border-left-color: #000 !important;
padding: 0 1px 0 0 !important;
}
.fc-event-vert .fc-event-inner {
border-width: 0 !important;
padding: 1px 0 !important;
}
.fc-event-bg {
display: none !important;
}
.fc-event .ui-resizable-handle {
display: none !important;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 896 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 888 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 746 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 869 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 807 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View file

@ -0,0 +1,171 @@
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml">
<head>
<title><roundcube:object name="pagetitle" /></title>
<roundcube:include file="/includes/links.html" />
<script type="text/javascript" src="/functions.js"></script>
</head>
<body class="calendarmain">
<roundcube:include file="/includes/taskbar.html" />
<roundcube:include file="/includes/header.html" />
<div id="main">
<div id="sidebar">
<div id="datepicker"></div>
<div id="calendars">
<div class="boxtitle"><roundcube:label name="calendar.calendars" /></div>
<div class="boxlistcontent">
<roundcube:object name="plugin.calendar_list" id="calendarslist" />
</div>
<div class="boxfooter">
<roundcube:button command="plugin.createcalendar" type="link" title="calendar.createcalendar" class="buttonPas addgroup" classAct="button addgroup" content=" " />
<roundcube:button name="calendarmenulink" id="calendarmenulink" type="link" title="calendaractions" class="button groupactions" onclick="return false" content=" " />
</div>
</div>
</div>
<div id="sidebartoggle"></div>
<div id="calendar"></div>
</div>
<div id="eventshow">
<h1 id="event-title">Event Title</h1>
<div class="event-section" id="event-location">Location</div>
<div class="event-section" id="event-date">From-To</div>
<div class="event-section" id="event-description">
<h5 class="label"><roundcube:label name="calendar.description" /></h5>
<div class="event-text"></div>
</div>
<div class="event-section" id="event-repeat">
<h5 class="label"><roundcube:label name="calendar.repeat" /></h5>
<div class="event-text"></div>
</div>
<div class="event-section" id="event-alarm">
<h5 class="label"><roundcube:label name="calendar.alam" /></h5>
<div class="event-text"></div>
</div>
<div class="event-section" id="event-attendees">
<h5 class="label"><roundcube:label name="calendar.tabattendees" /></h5>
<div class="event-text"></div>
</div>
<div class="event-line" id="event-calendar">
<label><roundcube:label name="calendar.calendar" /></label>
<span class="event-text">Default</span>
</div>
<div class="event-line" id="event-category">
<label><roundcube:label name="calendar.category" /></label>
<span class="event-text"></span>
</div>
<div class="event-line" id="event-free-busy">
<label><roundcube:label name="calendar.freebusy" /></label>
<span class="event-text"></span>
</div>
<div class="event-line" id="event-priority">
<label><roundcube:label name="calendar.priority" /></label>
<span class="event-text"></span>
</div>
</div>
<div id="eventedit">
<form id="eventtabs" action="#">
<ul>
<li><a href="#event-tab-1"><roundcube:label name="calendar.tabsummary" /></a></li>
<li id="edit-tab-recurrence"><a href="#event-tab-2"><roundcube:label name="calendar.tabrecurrence" /></a></li>
<li id="edit-tab-attendees"><a href="#event-tab-3"><roundcube:label name="calendar.tabattendees" /></a></li>
<li id="edit-tab-attachments"><a href="#event-tab-4"><roundcube:label name="calendar.tabattachments" /></a></li>
</ul>
<!-- basic info -->
<div id="event-tab-1">
<div class="event-section">
<label for="edit-title"><roundcube:label name="calendar.title" /></label>
<br />
<input type="text" class="text" name="title" id="edit-title" size="40" />
</div>
<div class="event-section">
<label for="edit-location"><roundcube:label name="calendar.location" /></label>
<br />
<input type="text" class="text" name="location" id="edit-location" size="40" />
</div>
<div class="event-section">
<label for="edit-description"><roundcube:label name="calendar.description" /></label>
<br />
<textarea name="description" id="edit-description" class="text" rows="5" cols="40"></textarea>
</div>
<div class="event-section">
<label style="float:right;padding-right:0.5em"><input type="checkbox" name="allday" id="edit-allday" value="1" /><roundcube:label name="calendar.all-day" /></label>
<label for="edit-startdate"><roundcube:label name="calendar.start" /></label>
<input type="text" name="startdate" size="10" id="edit-startdate" /> &nbsp;
<input type="text" name="starttime" size="6" id="edit-starttime" />
</div>
<div class="event-section">
<label for="edit-enddate"><roundcube:label name="calendar.end" /></label>
<input type="text" name="enddate" size="10" id="edit-enddate" /> &nbsp;
<input type="text" name="endtime" size="6" id="edit-endtime" />
</div>
<div class="event-section">
<label for="edit-alarm"><roundcube:label name="calendar.alarms" /></label>
<roundcube:object name="plugin.alarm_select" id="edit-alarm" />
</div>
<div class="event-section" id="calendar-select">
<label for="edit-calendar"><roundcube:label name="calendar.calendar" /></label>
<roundcube:object name="plugin.calendar_select" id="edit-calendar" />
</div>
<div class="event-section">
<label for="edit-categories"><roundcube:label name="calendar.category" /></label>
<roundcube:object name="plugin.category_select" id="edit-categories" />
</div>
<div class="event-section">
<label for="edit-free-busy"><roundcube:label name="calendar.freebusy" /></label>
<roundcube:object name="plugin.freebusy_select" id="edit-free-busy" />
</div>
<div class="event-section">
<label for="edit-priority"><roundcube:label name="calendar.priority" /></label>
<roundcube:object name="plugin.priority_select" id="edit-priority" />
</div>
</div>
<!-- recurrence settings -->
<div id="event-tab-2">
<div class="event-section border-after">
<roundcube:object name="plugin.recurrence_form" part="frequency" />
</div>
<div class="recurrence-form border-after" id="recurrence-form-daily">
<roundcube:object name="plugin.recurrence_form" part="daily" class="event-section" />
</div>
<div class="recurrence-form border-after" id="recurrence-form-weekly">
<roundcube:object name="plugin.recurrence_form" part="weekly" class="event-section" />
</div>
<div class="recurrence-form border-after" id="recurrence-form-monthly">
<roundcube:object name="plugin.recurrence_form" part="monthly" class="event-section" />
</div>
<div class="recurrence-form border-after" id="recurrence-form-yearly">
<roundcube:object name="plugin.recurrence_form" part="yearly" class="event-section" />
</div>
<div class="recurrence-form" id="recurrence-form-until">
<roundcube:object name="plugin.recurrence_form" part="until" class="event-section" />
</div>
</div>
<!-- attendees list -->
<div id="event-tab-3">
</div>
<!-- attachments list -->
<div id="event-tab-4">
</div>
</form>
</div>
<div id="calendartoolbar">
<roundcube:button command="plugin.addevent" type="link" class="buttonPas addevent" classAct="button addevent" classSel="button addeventSel" title="calendar.new_event" content=" " />
<roundcube:button command="plugin.print" type="link" class="buttonPas print" classAct="button print" classSel="button printSel" title="calendar.print" content=" " />
<roundcube:button command="plugin.export" type="link" class="buttonPas export" classAct="button export" classSel="button exportSel" title="calendar.export" content=" " />
<roundcube:container name="toolbar" id="calendartoolbar" />
</div>
<div id="quicksearchbar">
<roundcube:button name="searchmenulink" id="searchmenulink" image="/images/icons/glass.png" />
<roundcube:object name="searchform" id="quicksearchbox" />
<roundcube:button command="reset-search" id="searchreset" image="/images/icons/reset.gif" title="resetsearch" />
</div>
<roundcube:object name="plugin.calendar_css" />
</body>
</html>