Compare commits

...

43 commits

Author SHA1 Message Date
JodliDev
f154953979
Fix for #39 (color not sent in props) 2023-04-27 12:41:56 +02:00
JodliDev
43c30995b0
Merge pull request #34 from duburcqa/patch-1
Only skip update if the set of available calendars did not changed
Closes #13
2023-02-21 15:27:59 +01:00
Alexis DUBURCQ
61eb3a4a16
Only skip update if the set of available calendars did not changed.
This is necessary to automatically update sources for which the calendars have been added or deleted by another client than roundcube. It is typically the case when managing sharing access in nextcloud while still being able to view them in roundcube.
2022-12-28 01:14:59 +01:00
JodliDev
f0a90df1d3
Merge pull request #5 from club-1/feat-preinstalled-sources
feat: add preinstalled sources
2022-05-17 16:00:44 +02:00
JodliDev
3fa3b47e06 Fix #6: Correct inclusive end date of caldav for allday events 2022-05-16 09:30:57 +02:00
JodliDev
ad31194e03 Fix #12: Do not urlencode calendar.href 2022-05-13 19:23:13 +02:00
JodliDev
9c6b8b4171
Merge pull request #4 from club-1/fix-edit-event
fix: event edition for caldav driver
2022-05-12 01:24:21 +02:00
n-peugnet
f679c616e8 add caldav to preinstalled_sources key & example config 2022-05-02 21:33:56 +02:00
n-peugnet
847aae1321 refactor: preinstalled sources feature
move logic from calendar.php to caldav_driver.php
2022-05-01 21:42:54 +02:00
JodliDev
89cdebc5ca
Merge pull request #7 from club-1/fix-create-calendar-trailing-slash
fix: create calendars with source ending with '/'
2022-04-30 11:07:45 +02:00
JodliDev
b9ff57cf21
Merge pull request #3 from club-1/fix-postgres-timestamp
fix: unixtimestamp calculation from PostgreSQL
2022-04-30 11:03:59 +02:00
JodliDev
a6ec0c3b68
Merge pull request #2 from club-1/fix-postgres-sql
fix: postgres SQL script errors on initdb
2022-04-30 10:59:18 +02:00
n-peugnet
0e84caaa4e fix: create calendars with source ending with '/'
by trimming the trailing slash if it exists
2022-04-28 20:15:22 +02:00
n-peugnet
4e72a2b60f fix: event edition for caldav driver
Set to NULL for real in db values that equals NULL in PHP.

Only tested on PostgreSQL
2022-04-28 14:36:26 +02:00
n-peugnet
8347d65795 feat: add preinstalled sources 2022-04-28 14:34:13 +02:00
n-peugnet
ee19875c97 fix: unixtimestamp calculation from PostgreSQL
see https://stackoverflow.com/questions/29536542/different-results-for-extract-epoch-on-different-postgresql-servers
2022-04-28 14:21:43 +02:00
n-peugnet
2d72a2edcb fix: postgres SQL script errors on initdb 2022-04-28 14:18:06 +02:00
JodliDev
64599f4cfe
Update README.md 2022-04-25 14:35:02 +02:00
JodliDev
927bf88862
Update README.md 2022-04-25 13:59:44 +02:00
JodliDev
2a0c9f8c07 Copy and paste error when including
https://git.kolab.org/rRPK3613a3d39999d67e46716658718c468bc23480e7
2022-04-25 13:56:18 +02:00
JodliDev
91a976b444 See https://git.kolab.org/rRPKdc99ade020f90dc37d84e3ee7d0a791bac0ca849 2022-04-25 11:14:19 +02:00
JodliDev
9caeea244a See https://git.kolab.org/rRPKde3a536daa605d07d47cb1b24af1d8042d049135 2022-04-25 10:08:24 +02:00
JodliDev
ca2409e081 See https://git.kolab.org/rRPK3613a3d39999d67e46716658718c468bc23480e7 2022-04-25 10:03:39 +02:00
JodliDev
c1f5524cec See https://git.kolab.org/rRPKea78b1c2df4f655c571f872e66e489e6ae531d11 2022-04-25 09:52:39 +02:00
JodliDev
94a287afc6 See https://git.kolab.org/rRPK38b103430bf838061ef78c804e225c36a74ee217 2022-04-25 09:43:10 +02:00
JodliDev
72e7983d75 (botchy) fix for emoticons in event description 2021-09-30 07:01:11 +02:00
JodliDev
72dd4dffcd Several bugfixes and README-update 2021-09-28 21:23:22 +02:00
JodliDev
0fe9be1221 wrong version for libcalendaring v2 (forgot prefix) 2021-09-28 11:00:01 +02:00
JodliDev
f9e2c2667f wrong version for libcalendaring 2021-09-28 10:52:56 +02:00
JodliDev
a782469f11 Merge branch 'mail_import_select_calendar' 2021-08-27 11:33:31 +02:00
JodliDev
b68b5662a1 Merge branch 'caldav_driver' 2021-08-27 11:33:13 +02:00
JodliDev
4100dadd69 Add dropdown options for each calendar 2021-08-27 11:32:38 +02:00
JodliDev
71c5fc85a0 Adapt configs 2021-08-27 09:46:02 +02:00
JodliDev
71c4d7aba9 Allow to add ics calendars when no CalDAV sources exist 2021-08-26 16:26:17 +02:00
JodliDev
3df123b0b8 implement the attributes "deletable" and "editable_name" for calendars 2021-08-26 15:58:25 +02:00
JodliDev
766088b19a Save HEX colors without alpha 2021-08-26 11:58:24 +02:00
JodliDev
263e1cc1f3 use markdown 2021-08-25 12:20:30 +02:00
JodliDev
cf57748f4c Added rationale for this fork 2021-08-25 12:18:36 +02:00
JodliDev
0cc7f1ebdd - make ical calendars readonly
- small attachement-bugfix
2021-08-25 10:43:52 +02:00
JodliDev
277e3094f7 - Added iCal Support
- Bugfixes
- automatically generated sqlite & postgres support (not tested!)
2021-08-24 12:55:41 +02:00
JodliDev
5ee16ce9ef Lots of bugfixes regarding CalDAV 2021-08-23 08:58:32 +02:00
JodliDev
822b359185 first commit of new changes to CalDAV support 2021-08-20 19:36:47 +02:00
JodliDev
21e3fff576 Copy CadDav driver from https://github.com/fasterit/roundcube_calendar 2021-08-17 19:19:48 +02:00
26 changed files with 3971 additions and 170 deletions

81
README
View file

@ -1,81 +0,0 @@
A calendar module for Roundcube
-------------------------------
This plugin currently supports a local database as well as a Kolab groupware
server as backends for calendar and event storage. For both drivers, some
initialization of the local database is necessary. To do so, execute the
SQL commands in drivers/<yourchoice>/SQL/<yourdatabase>.initial.sql
For some general calendar-based operations such as alarms handling or iCal
parsing/exporting and UI widgets/style this plugins requires the `libcalendaring`
and `libkolab` plugins which are also part of the Kolab Roundcube Plugins repository.
Make sure these plugins are installed and configured correctly.
For recurring event computation, some utility classes from the Horde project
are used. They are packaged in a slightly modified version with this plugin.
REQUIREMENTS
------------
Some functions are shared with other plugins and therefore being moved to
library plugins. Thus in order to run the calendar plugin, you also need the
following plugins installed:
* kolab/libcalendaring [1]
* kolab/libkolab [1]
INSTALLATION
------------
For a manual installation of the calendar plugin (and its dependencies),
execute the following steps. This will set it up with the database backend
driver.
1. Get the source from git
$ cd /tmp
$ git clone https://git.kolab.org/diffusion/RPK/roundcubemail-plugins-kolab.git
$ cd /<path-to-roundcube>/plugins
$ cp -r /tmp/roundcubemail-plugins-kolab/plugins/calendar .
$ cp -r /tmp/roundcubemail-plugins-kolab/plugins/libcalendaring .
$ cp -r /tmp/roundcubemail-plugins-kolab/plugins/libkolab .
2. Create calendar plugin configuration
$ cd calendar/
$ cp config.inc.php.dist config.inc.php
$ edit config.inc.php
3. Initialize the calendar database tables
$ cd ../../
$ bin/initdb.sh --dir=plugins/calendar/drivers/database/SQL
4. Build css styles for the Elastic skin
$ lessc --relative-urls -x plugins/libkolab/skins/elastic/libkolab.less > plugins/libkolab/skins/elastic/libkolab.min.css
5. Enable the calendar plugin
$ edit config/config.inc.php
Add 'calendar' to the list of active plugins:
$config['plugins'] = array(
(...)
'calendar',
);
IMPORTANT
---------
This plugin doesn't work with the Classic skin of Roundcube because no
templates are available for that skin.
Use Roundcube `skins_allowed` option to limit skins available to the user
or remove incompatible skins from the skins folder.
[1] https://git.kolab.org/diffusion/RPK/

68
README.md Normal file
View file

@ -0,0 +1,68 @@
## TLDR
Contrary to other CalDAV forks, this one is based on the original calendar kolab-roundcube-plugins-mirror/calendar (which means the calendar itself is most up to date) and adds CalDAV capability on top of it. As far as I am aware, it is the most up to date version with the most bugfixes (April 2022).
(installation instructions are at the bottom)
## Why is this needed?
Unfortunately, the current situation about CalDAV support in roundcube is quite confusing. There are several plugins (/Forks) around that have CalDAV support but from what I found, all of them are slightly buggy or do not work anymore.
All of them are based on <https://gitlab.awesome-it.de/kolab/roundcube-plugins>, a very old Fork which (as far as I can tell) is based on a version of kolab-roundcube-plugins-mirror/calendar that is over 10 years old.
None of these forks incorporate updates from the original calendar, meaning it is only a matter of time until they are not compatible with roundcube anymore. I tried to change as little as possible in the original codebase and only added caldav support as a new driver - which means new updates from the roundcube team should be easy to incorporate.
## History of other Forks so far
### kolab-roundcube-plugins-mirror/calendar :
This is the original calendar that all other forks are based on. It is working very well and is actively maintained but unfortunately, it does not have caldav support
### [https://gitlab.awesome-it.de/kolab/roundcube-plugins](awesome-it) :
This is the "original fork" of the calendar. A lot of work was put into it and caldav is almost fully implemented. Unfortunately it has a few bugs / problems and most of them were not fixed in any other forks:
- The birthday calendar is not supported by the caldav driver.
- While the backend (mostly) supports adding all calendars from a dav-url, the front-end does not. That makes calendar handling a bit clunky and confusing.
- Calendar colors have to be set manually and can not be loaded from DAV.
- Adding and Removing calendars directly in the external source is not supported.
- It prepares the codebase so multiple drivers can be used. But as far as I can tell, this feature is not used in the code and also not really supported by the front-end. This means, that it still only uses one driver but as a result adds a lot of unnecessary changes to the original codebase.
### fasterit/roundcube_calendar :
A fork of awesome-it to make it work with blind-coder/rcmcardav (a CardDAV plugin) by packing the outdated version of sabre/DAV inside the plugin. But it hasn't been maintained and is still based on a very outdated version of kolab-roundcube-plugins-mirror/calendar.
### texxasrulez/calendar :
This is a fork of awesome-it with a few bugfixes to make it work with roundcube 1.3 but its maintainer does not seem to be active anymore.
It is the most current fork of the original CalDAV fork. But unfortunately, it is treated as its own project (which means that it doesn't have any updates from the original calendar) and is focused primarily on nextcloud (which I don't really understand since nextcloud is using CalDAV anyway).
Also, on top of still having the original bugs included, it is also still based on an ancient sabre/DAV version.
### texxasrulez/caldav_calendar :
That one confuses me. It is from texxasrulez as well and seems to be the basis of Texxas but was abandoned in favour of texxasrulez/calendar. But it seems to be only a few commits behind texxasrulez/calendar.
### What is this fork doing differently?
All CalDAV forks are based on faster-it which has a very different codebase to the original calendar because of its unfinished "multiple-driver" support. That makes it very difficult to get updates from the original calendar.
So I decided to ditch the "multiple driver" support (which isnt used anywway) and keep most changes in the CalDAV driver itself to stay compatible with the original calendar. I also added a ton of updates:
- Based on the most recent version of the calendar plugin.
- Uses the most recent version of sabre/dav (4.1.5)
- Only minor changes in the existing code base, meaning that future updates of the calendar plugin should be able to be merged quite easily.
- Added support for the birthday calendar.
- Changed the behaviour from "per calendar" to "per CalDAV source".
- All calendars from a source will be automatically added.
- Calendars can be created and deleted directly at the CalDAV source.
- ics support included.
### Why does this need a fork of libcalendaring?
The original libcalendaring still uses sabre/vobject 3.5.3
In order to be compatible with other plugins (and because version 3.5.3 is ancient), I updated it to version 4.1.5
The problem is, that sabre/vobject makes use of DateTimeImmutable which libcalendaring does not expect.
It only needs minor changes to account for that, but unfortunately the roundcube-project does not accept pull requests...
### Installation
I havent published this as a plugin yet, so you have to instruct composer to install directly from github. Run the following commands in the roundcubemail folder
(If you get an error that the "API rate limit" has been exceeded and you need an GitHub OAuth token, just follow the instructions in the console - you will need a GitHub account).
```
cd /pathTo/roundcubemail
composer config repositories.calendar vcs https://github.com/JodliDev/calendar
composer config repositories.libcalendaring vcs https://github.com/JodliDev/libcalendaring
composer config minimum-stability dev
composer require kolab/calendar
bin/initdb.sh --dir=plugins/calendar/drivers/caldav/SQL
```

View file

@ -928,6 +928,18 @@ $("#rcmfd_new_category").keypress(function(event) {
} }
switch ($action) { switch ($action) {
case "new-source": //we "misuse" create_calendar() to stay compatible with other drivers
$cal['new-source'] = true;
$success = $this->driver->create_calendar($cal);
$reload = true;
break;
case "delete-source": //we "misuse" delete_calendar() to stay compatible with other drivers
$cal['delete-source'] = true;
$success = $this->driver->delete_calendar($cal);
$reload = true;
break;
case "form-source-new": //we "misuse" calendar_editform() to stay compatible with other drivers
case "form-source-delete": //we "misuse" calendar_editform() to stay compatible with other drivers
case "form-new": case "form-new":
case "form-edit": case "form-edit":
echo $this->ui->calendar_editform($action, $cal); echo $this->ui->calendar_editform($action, $cal);
@ -1239,23 +1251,31 @@ $("#rcmfd_new_category").keypress(function(event) {
$noreply = intval($noreply) || $status == 'needs-action' || $itip_sending === 0; $noreply = intval($noreply) || $status == 'needs-action' || $itip_sending === 0;
$reload = $event['calendar'] != $ev['calendar'] || !empty($event['recurrence']) ? 2 : 1; $reload = $event['calendar'] != $ev['calendar'] || !empty($event['recurrence']) ? 2 : 1;
$emails = $this->get_user_emails(); $emails = $this->get_user_emails();
$ownedResourceEmails = $this->owned_resources_emails();
$organizer = null; $organizer = null;
$resourceConfirmation = false;
foreach ($event['attendees'] as $i => $attendee) { foreach ($event['attendees'] as $i => $attendee) {
if ($attendee['role'] == 'ORGANIZER') { if ($attendee['role'] == 'ORGANIZER') {
$organizer = $attendee; $organizer = $attendee;
} }
else if (!empty($attendee['email']) && in_array(strtolower($attendee['email']), $emails)) { else if (!empty($attendee['email']) && in_array_nocase($attendee['email'], $emails)) {
$reply_sender = $attendee['email']; $reply_sender = $attendee['email'];
} }
else if (!empty($attendee['cutype']) && $attendee['cutype'] == 'RESOURCE' && !empty($attendee['email']) && in_array_nocase($attendee['email'], $ownedResourceEmails)) {
$resourceConfirmation = true;
// Note on behalf of which resource this update is going to be sent out
$event['_resource'] = $attendee['email'];
}
} }
if (!$noreply) { if (!$noreply) {
$itip = $this->load_itip(); $itip = $this->load_itip();
$itip->set_sender_email($reply_sender); $itip->set_sender_email($reply_sender);
$event['thisandfuture'] = $event['_savemode'] == 'future'; $event['thisandfuture'] = $event['_savemode'] == 'future';
$bodytextprefix = $resourceConfirmation ? 'itipmailbodyresource' : 'itipmailbody';
if ($organizer && $itip->send_itip_message($event, 'REPLY', $organizer, 'itipsubject' . $status, 'itipmailbody' . $status)) { if ($organizer && $itip->send_itip_message($event, 'REPLY', $organizer, 'itipsubject' . $status, $bodytextprefix . $status)) {
$mailto = !empty($organizer['name']) ? $organizer['name'] : $organizer['email']; $mailto = !empty($organizer['name']) ? $organizer['name'] : $organizer['email'];
$msg = $this->gettext(['name' => 'sentresponseto', 'vars' => ['mailto' => $mailto]]); $msg = $this->gettext(['name' => 'sentresponseto', 'vars' => ['mailto' => $mailto]]);
@ -2006,10 +2026,12 @@ $("#rcmfd_new_category").keypress(function(event) {
} }
$identity['emails'][] = $this->rc->user->get_username(); $identity['emails'][] = $this->rc->user->get_username();
$identity['ownedResources'] = $this->owned_resources_emails();
$settings['identity'] = [ $settings['identity'] = [
'name' => $identity['name'], 'name' => $identity['name'],
'email' => strtolower($identity['email']), 'email' => strtolower($identity['email']),
'emails' => ';' . strtolower(join(';', $identity['emails'])) 'emails' => ';' . strtolower(join(';', $identity['emails'])),
'ownedResources' => ';' . strtolower(join(';', $identity['ownedResources']))
]; ];
} }
@ -2390,7 +2412,7 @@ $("#rcmfd_new_category").keypress(function(event) {
} }
// set new organizer identity // set new organizer identity
if ($organizer !== false && $identity) { if ($organizer !== false && !empty($identity)) {
$event['attendees'][$organizer]['name'] = $identity['name']; $event['attendees'][$organizer]['name'] = $identity['name'];
$event['attendees'][$organizer]['email'] = $identity['email']; $event['attendees'][$organizer]['email'] = $identity['email'];
} }
@ -2400,7 +2422,7 @@ $("#rcmfd_new_category").keypress(function(event) {
unset($event['attendees'][$owner]['rsvp']); unset($event['attendees'][$owner]['rsvp']);
} }
// fallback to the selected identity // fallback to the selected identity
else if ($organizer === false && $identity) { else if ($organizer === false && !empty($identity)) {
$event['attendees'][] = [ $event['attendees'][] = [
'role' => 'ORGANIZER', 'role' => 'ORGANIZER',
'name' => $identity['name'], 'name' => $identity['name'],
@ -2867,6 +2889,21 @@ $("#rcmfd_new_category").keypress(function(event) {
exit; exit;
} }
/**
* List email addressed of owned resources
*/
private function owned_resources_emails()
{
$results = [];
if ($directory = $this->resources_directory()) {
foreach ($directory->load_resources($_SESSION['kolab_dn'], 5000, 'owner') as $rec) {
$results[] = $rec['email'];
}
}
return $results;
}
/**** Event invitation plugin hooks ****/ /**** Event invitation plugin hooks ****/
/** /**
@ -3148,10 +3185,16 @@ $("#rcmfd_new_category").keypress(function(event) {
*/ */
private function mail_agenda_event_row($event, $class = '') private function mail_agenda_event_row($event, $class = '')
{ {
$time = !empty($event['allday']) ? $this->gettext('all-day') : if (!empty($event['allday'])) {
$this->rc->format_date($event['start'], $this->rc->config->get('time_format')) $time = $this->gettext('all-day');
. ' - ' . }
$this->rc->format_date($event['end'], $this->rc->config->get('time_format')); else {
$start = is_object($event['start']) ? clone $event['start'] : $event['start'];
$end = is_object($event['end']) ? clone $event['end'] : $event['end'];
$time = $this->rc->format_date($start, $this->rc->config->get('time_format'))
. ' - ' . $this->rc->format_date($end, $this->rc->config->get('time_format'));
}
return html::div(rtrim('event-row ' . ($class ?: $event['className'])), return html::div(rtrim('event-row ' . ($class ?: $event['className'])),
html::span('event-date', $time) html::span('event-date', $time)
@ -3216,7 +3259,7 @@ $("#rcmfd_new_category").keypress(function(event) {
// get prepared inline UI for this event object // get prepared inline UI for this event object
if ($ical_objects->method) { if ($ical_objects->method) {
$append = ''; $append = '';
$date_str = $this->rc->format_date($event['start'], $this->rc->config->get('date_format'), empty($event['start']->_dateonly)); $date_str = $this->rc->format_date(clone $event['start'], $this->rc->config->get('date_format'), empty($event['start']->_dateonly));
$date = new DateTime($event['start']->format('Y-m-d') . ' 12:00:00', new DateTimeZone('UTC')); $date = new DateTime($event['start']->format('Y-m-d') . ' 12:00:00', new DateTimeZone('UTC'));
// prepare a small agenda preview to be filled with actual event data on async request // prepare a small agenda preview to be filled with actual event data on async request
@ -3254,19 +3297,27 @@ $("#rcmfd_new_category").keypress(function(event) {
// add "Save to calendar" button into attachment menu // add "Save to calendar" button into attachment menu
if ($has_events) { if ($has_events) {
$this->add_button([ $calendars = $this->driver->list_calendars(calendar_driver::FILTER_PERSONAL | calendar_driver::FILTER_WRITEABLE);
'id' => 'attachmentsavecal',
'name' => 'attachmentsavecal', foreach($calendars as $calendar) {
'type' => 'link', //TODO inline style tags are a bit ugly, but I cant think of a better way of coloring the icon
'wrapper' => 'li', $style = html::tag('style', ['type' => 'text/css'], ".icon.cal-$calendar[id]::before {color: #$calendar[color]}");
'command' => 'attachment-save-calendar', $this->add_button([
'class' => 'icon calendarlink disabled', 'id' => 'attachmentsavecal-' .$calendar['id'],
'classact' => 'icon calendarlink active', 'name' => 'attachmentsavecal',
'type' => 'link',
'wrapper' => 'li',
'command' => 'attachment-save-calendar',
'prop' => $calendar['id'],
'class' => 'icon calendarlink disabled',
'classact' => "icon calendarlink active cal-$calendar[id]",
'innerclass' => 'icon calendar', 'innerclass' => 'icon calendar',
'label' => 'calendar.savetocalendar', //maybe change the calendar.savetocalendar translation so a calendar can be added and use a label instead?
'content' => $this->gettext('calendar.savetocalendar') ." ($calendar[name]) $style"
], ],
'attachmentmenu' 'attachmentmenu'
); );
}
} }
return $p; return $p;
@ -3405,39 +3456,38 @@ $("#rcmfd_new_category").keypress(function(event) {
// only update attendee status // only update attendee status
if ($event['_method'] == 'REPLY') { if ($event['_method'] == 'REPLY') {
// try to identify the attendee using the email sender address $existing_attendee_index = -1;
$existing_attendee = -1;
$existing_attendee_emails = [];
foreach ($existing['attendees'] as $i => $attendee) {
$existing_attendee_emails[] = $attendee['email'];
if ($this->itip->compare_email($attendee['email'], $event['_sender'], $event['_sender_utf'])) {
$existing_attendee = $i;
}
}
$event_attendee = null; $event_attendee = null;
$update_attendees = []; $update_attendees = [];
foreach ($event['attendees'] as $attendee) { if ($attendee = $this->itip->find_reply_attendee($event)) {
if ($this->itip->compare_email($attendee['email'], $event['_sender'], $event['_sender_utf'])) { $event_attendee = $attendee;
$event_attendee = $attendee; $update_attendees[] = $attendee;
$update_attendees[] = $attendee; $metadata['fallback'] = $attendee['status'];
$metadata['fallback'] = $attendee['status']; $metadata['attendee'] = $attendee['email'];
$metadata['attendee'] = $attendee['email']; $metadata['rsvp'] = !empty($attendee['rsvp']) || $attendee['role'] != 'NON-PARTICIPANT';
$metadata['rsvp'] = !empty($attendee['rsvp']) || $attendee['role'] != 'NON-PARTICIPANT';
if ($attendee['status'] != 'DELEGATED') { $existing_attendee_emails = [];
break;
// Find the attendee to update
foreach ($existing['attendees'] as $i => $existing_attendee) {
$existing_attendee_emails[] = $existing_attendee['email'];
if ($this->itip->compare_email($existing_attendee['email'], $attendee['email'])) {
$existing_attendee_index = $i;
} }
} }
// also copy delegate attendee
else if (!empty($attendee['delegated-from']) if ($attendee['status'] == 'DELEGATED') {
&& $this->itip->compare_email($attendee['delegated-from'], $event['_sender'], $event['_sender_utf']) //Also find and copy the delegatee
) { $delegatee_email = $attendee['email'];
$update_attendees[] = $attendee; $delegatees = array_filter($event['attendees'], function($attendee) use ($delegatee_email){ return $attendee['role'] != 'ORGANIZER' && $this->itip->compare_email($attendee['delegated-from'], $delegatee_email); });
if (!in_array_nocase($attendee['email'], $existing_attendee_emails)) {
$existing['attendees'][] = $attendee; if ($delegatee = $this->itip->find_attendee_by_email($event['attendees'], 'delegated-from', $attendee['email'])) {
$update_attendees[] = $delegatee;
if (!in_array_nocase($delegatee['email'], $existing_attendee_emails)) {
$existing['attendees'][] = $delegated_attendee;
}
} }
} }
} }
@ -3455,29 +3505,9 @@ $("#rcmfd_new_category").keypress(function(event) {
} }
} }
// Accept sender as a new participant (different email in From: and the iTip)
// Use ATTENDEE entry from the iTip with replaced email address
if (!$event_attendee) {
// remove the organizer
$itip_attendees = array_filter(
$event['attendees'],
function($item) { return $item['role'] != 'ORGANIZER'; }
);
// there must be only one attendee
if (is_array($itip_attendees) && count($itip_attendees) == 1) {
$event_attendee = $itip_attendees[key($itip_attendees)];
$event_attendee['email'] = $event['_sender'];
$update_attendees[] = $event_attendee;
$metadata['fallback'] = $event_attendee['status'];
$metadata['attendee'] = $event_attendee['email'];
$metadata['rsvp'] = !empty($event_attendee['rsvp']) || $event_attendee['role'] != 'NON-PARTICIPANT';
}
}
// found matching attendee entry in both existing and new events // found matching attendee entry in both existing and new events
if ($existing_attendee >= 0 && $event_attendee) { if ($existing_attendee_index >= 0 && $event_attendee) {
$existing['attendees'][$existing_attendee] = $event_attendee; $existing['attendees'][$existing_attendee_index] = $event_attendee;
$success = $this->driver->update_attendees($existing, $update_attendees); $success = $this->driver->update_attendees($existing, $update_attendees);
} }
// update the entire attendees block // update the entire attendees block

View file

@ -72,13 +72,12 @@ function rcube_calendar(settings)
// handler for attachment-save-calendar commands // handler for attachment-save-calendar commands
this.save_to_calendar = function(p) this.save_to_calendar = function(p)
{ {
// TODO: show dialog to select the calendar for importing
if (this.selected_attachment && window.rcube_libcalendaring) { if (this.selected_attachment && window.rcube_libcalendaring) {
rcmail.http_post('calendar/mailimportattach', { rcmail.http_post('calendar/mailimportattach', {
_uid: rcmail.env.uid, _uid: rcmail.env.uid,
_mbox: rcmail.env.mailbox, _mbox: rcmail.env.mailbox,
_part: this.selected_attachment _part: this.selected_attachment,
// _calendar: $('#calendar-attachment-saveto').val(), _calendar: p
}, rcmail.set_busy(true, 'itip.savingdata')); }, rcmail.set_busy(true, 'itip.savingdata'));
} }
}; };
@ -93,7 +92,7 @@ window.rcmail && rcmail.addEventListener('init', function(evt) {
// register create-from-mail command to message_commands array // register create-from-mail command to message_commands array
if (rcmail.env.task == 'mail') { if (rcmail.env.task == 'mail') {
rcmail.register_command('calendar-create-from-mail', function() { cal.create_from_mail(); }); rcmail.register_command('calendar-create-from-mail', function() { cal.create_from_mail(); });
rcmail.register_command('attachment-save-calendar', function() { cal.save_to_calendar(); }); rcmail.register_command('attachment-save-calendar', function(p) { cal.save_to_calendar(p); });
if (rcmail.env.action != 'show') { if (rcmail.env.action != 'show') {
rcmail.env.message_commands.push('calendar-create-from-mail'); rcmail.env.message_commands.push('calendar-create-from-mail');

View file

@ -459,7 +459,7 @@ function rcube_calendar_ui(settings)
for (var j=0; j < num_attendees; j++) { for (var j=0; j < num_attendees; j++) {
data = event.attendees[j]; data = event.attendees[j];
if (data.email) { if (data.email) {
if (data.role != 'ORGANIZER' && settings.identity.emails.indexOf(';'+data.email) >= 0) { if (data.role != 'ORGANIZER' && is_this_me(data.email)) {
mystatus = (data.status || 'UNKNOWN').toLowerCase(); mystatus = (data.status || 'UNKNOWN').toLowerCase();
if (data.status == 'NEEDS-ACTION' || data.status == 'TENTATIVE' || data.rsvp) if (data.status == 'NEEDS-ACTION' || data.status == 'TENTATIVE' || data.rsvp)
rsvp = mystatus; rsvp = mystatus;
@ -2379,6 +2379,14 @@ function rcube_calendar_ui(settings)
add_attendee($.extend({ role:'REQ-PARTICIPANT', status:'NEEDS-ACTION', cutype:'RESOURCE' }, resource)); add_attendee($.extend({ role:'REQ-PARTICIPANT', status:'NEEDS-ACTION', cutype:'RESOURCE' }, resource));
} }
var is_this_me = function(email)
{
if (settings.identity.emails.indexOf(';'+email) >= 0 || settings.identity.ownedResources.indexOf(';'+email) >= 0) {
return true;
}
return false;
};
// when the user accepts or declines an event invitation // when the user accepts or declines an event invitation
var event_rsvp = function(response, delegate, replymode, event) var event_rsvp = function(response, delegate, replymode, event)
{ {
@ -2413,7 +2421,8 @@ function rcube_calendar_ui(settings)
attendees = []; attendees = [];
for (var data, i=0; i < me.selected_event.attendees.length; i++) { for (var data, i=0; i < me.selected_event.attendees.length; i++) {
data = me.selected_event.attendees[i]; data = me.selected_event.attendees[i];
if (settings.identity.emails.indexOf(';'+String(data.email).toLowerCase()) >= 0) { //FIXME this can only work if there is a single resource per invitation
if (is_this_me(String(data.email).toLowerCase())) {
data.status = response.toUpperCase(); data.status = response.toUpperCase();
data.rsvp = 0; // unset RSVP flag data.rsvp = 0; // unset RSVP flag
@ -2884,6 +2893,64 @@ function rcube_calendar_ui(settings)
return update_event_confirm('remove', event, { id:event.id, calendar:event.calendar, attendees:event.attendees }); return update_event_confirm('remove', event, { id:event.id, calendar:event.calendar, attendees:event.attendees });
}; };
//opens a dialog to add caldav sources
this.calendar_new_source = function() {
var title = rcmail.gettext('addsources', 'calendar'),
params = {action: 'form-source-new', _framed: 1},
$dialog = $('<iframe>').attr('src', rcmail.url('calendar', params)),
save_func = function() {
var data,
form = $dialog.contents().find('#calendarpropform');
// form is not loaded
if (!form || !form.length)
return false;
// post data to server
data = form.serializeJSON();
if (data.color)
data.color = data.color.replace(/^#/, '');
if (calendar.id)
data.id = calendar.id;
me.saving_lock = rcmail.set_busy(true, 'calendar.savingdata');
rcmail.http_post('calendar', { action:'new-source', c:data });
$dialog.dialog("close");
};
rcmail.simple_dialog($dialog, title, save_func, {
width: 600,
height: 400
});
};
//opens a dialog to delete caldav sources
this.calendar_delete_sources = function() {
var title = rcmail.gettext('deletesources', 'calendar'),
params = {action: 'form-source-delete', _framed: 1},
$dialog = $('<iframe>').attr('src', rcmail.url('calendar', params)),
save_func = function() {
var data,
form = $dialog.contents().find('#calendarpropform');
// form is not loaded
if (!form || !form.length)
return false;
// post data to server
data = form.serializeJSON();
me.saving_lock = rcmail.set_busy(true, 'calendar.savingdata');
rcmail.http_post('calendar', { action:'delete-source', c:data });
$dialog.dialog("close");
};
rcmail.simple_dialog($dialog, title, save_func, {
width: 600,
height: 400
});
};
// opens a jquery UI dialog with event properties (or empty for creating a new calendar) // opens a jquery UI dialog with event properties (or empty for creating a new calendar)
this.calendar_edit_dialog = function(calendar) this.calendar_edit_dialog = function(calendar)
{ {
@ -2895,7 +2962,7 @@ function rcube_calendar_ui(settings)
$dialog = $('<iframe>').attr('src', rcmail.url('calendar', params)).on('load', function() { $dialog = $('<iframe>').attr('src', rcmail.url('calendar', params)).on('load', function() {
var contents = $(this).contents(); var contents = $(this).contents();
contents.find('#calendar-name') contents.find('#calendar-name')
.prop('disabled', !calendar.editable) .prop('disabled', !calendar.editable && !calendar.editable_name)
.val(calendar.editname || calendar.name) .val(calendar.editname || calendar.name)
.select(); .select();
contents.find('#calendar-color') contents.find('#calendar-color')
@ -3549,7 +3616,7 @@ function rcube_calendar_ui(settings)
if (node && node.id && me.calendars[node.id]) { if (node && node.id && me.calendars[node.id]) {
me.select_calendar(node.id, true); me.select_calendar(node.id, true);
rcmail.enable_command('calendar-edit', 'calendar-showurl', 'calendar-showfburl', true); rcmail.enable_command('calendar-edit', 'calendar-showurl', 'calendar-showfburl', true);
rcmail.enable_command('calendar-delete', me.calendars[node.id].editable); rcmail.enable_command('calendar-delete', me.calendars[node.id].editable || me.calendars[node.id].deletable);
rcmail.enable_command('calendar-remove', me.calendars[node.id] && me.calendars[node.id].removable); rcmail.enable_command('calendar-remove', me.calendars[node.id] && me.calendars[node.id].removable);
} }
}); });
@ -4168,6 +4235,8 @@ window.rcmail && rcmail.addEventListener('init', function(evt) {
rcmail.register_command('print', function(){ cal.print_calendars(); }, true); rcmail.register_command('print', function(){ cal.print_calendars(); }, true);
// configure list operations // configure list operations
rcmail.register_command('calendar-sources-new', cal.calendar_new_source, true);
rcmail.register_command('calendar-sources-delete', cal.calendar_delete_sources, true);
rcmail.register_command('calendar-create', function(){ cal.calendar_edit_dialog(null); }, true); rcmail.register_command('calendar-create', function(){ cal.calendar_edit_dialog(null); }, true);
rcmail.register_command('calendar-edit', function(){ cal.calendar_edit_dialog(cal.calendars[cal.selected_calendar]); }, false); rcmail.register_command('calendar-edit', function(){ cal.calendar_edit_dialog(cal.calendars[cal.selected_calendar]); }, false);
rcmail.register_command('calendar-remove', function(){ cal.calendar_remove(cal.calendars[cal.selected_calendar]); }, false); rcmail.register_command('calendar-remove', function(){ cal.calendar_remove(cal.calendars[cal.selected_calendar]); }, false);

View file

@ -4,7 +4,7 @@
"description": "Calendar plugin", "description": "Calendar plugin",
"homepage": "https://git.kolab.org/diffusion/RPK/", "homepage": "https://git.kolab.org/diffusion/RPK/",
"license": "AGPLv3", "license": "AGPLv3",
"version": "3.5.7", "version": "3.5.11",
"authors": [ "authors": [
{ {
"name": "Thomas Bruederli", "name": "Thomas Bruederli",
@ -21,13 +21,18 @@
{ {
"type": "composer", "type": "composer",
"url": "https://plugins.roundcube.net" "url": "https://plugins.roundcube.net"
},
{
"type": "vcs",
"url": "https://github.com/JodliDev/libcalendaring"
} }
], ],
"require": { "require": {
"php": ">=5.4.0", "php": ">=5.5",
"roundcube/plugin-installer": ">=0.1.3", "roundcube/plugin-installer": ">=0.1.3",
"kolab/libcalendaring": ">=3.4.0", "jodlidev/libcalendaring": "dev-master",
"kolab/libkolab": ">=3.4.0" "kolab/libkolab": ">=3.4.0",
"sabre/dav": ">=4.1.5"
}, },
"extra": { "extra": {
"roundcube": { "roundcube": {

View file

@ -25,14 +25,18 @@
+-------------------------------------------------------------------------+ +-------------------------------------------------------------------------+
*/ */
// backend type (database, kolab) // backend type (database, kolab, caldav)
$config['calendar_driver'] = "database"; $config['calendar_driver'] = "caldav";
// Enable debugging output for iCAL/CalDAV drivers
$config['calendar_caldav_debug'] = false;
// default calendar view (agendaDay, agendaWeek, month) // default calendar view (agendaDay, agendaWeek, month)
$config['calendar_default_view'] = "agendaWeek"; $config['calendar_default_view'] = "agendaWeek";
// show a birthdays calendar from the user's address book(s) // show a birthdays calendar from the user's address book(s)
$config['calendar_contact_birthdays'] = false; $config['calendar_contact_birthdays'] = false;
$config['birthday_calendar'] = array('color' => 'fffb00');
// timeslots per hour (1, 2, 3, 4, 6) // timeslots per hour (1, 2, 3, 4, 6)
$config['calendar_timeslots'] = 2; $config['calendar_timeslots'] = 2;
@ -139,6 +143,20 @@ $config['kolab_invitation_calendars'] = false;
// %i - Calendar UUID // %i - Calendar UUID
// $config['calendar_caldav_url'] = 'http://%h/iRony/calendars/%u/%i'; // $config['calendar_caldav_url'] = 'http://%h/iRony/calendars/%u/%i';
// List of CalDAV sources that should be allready installed.
// They will be added when the calendar section is accessed for the first time by a user.
// For 'caldav_user' and 'caldav_url' the following replacement variables are supported:
// %u - Current webmail user name
// For 'caldav_pass' %p is replaced by the current user's password.
// $config['calendar_caldav_preinstalled_sources'] = array(
// 'name' => array(
// 'caldav_user' => '%u',
// 'caldav_pass' => '%p',
// 'caldav_url' => 'https://example.net/dav',
// 'showAlarms' => 1
// )
// );
// Driver to provide a resource directory ('ldap' is the only implementation yet). // Driver to provide a resource directory ('ldap' is the only implementation yet).
// Leave empty or commented to disable resources support. // Leave empty or commented to disable resources support.
// $config['calendar_resources_driver'] = 'ldap'; // $config['calendar_resources_driver'] = 'ldap';

75
drivers/caldav/Isync.php Normal file
View file

@ -0,0 +1,75 @@
<?php
/**
* Interface for different sync drivers
*
* @version @package_version@
* @author JodliDev <jodlidev@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
interface Isync {
/**
* Getter for current calendar ctag (only for CalDAV).
* @return string
*/
public function get_ctag();
/**
* Determines whether current calendar needs to be synced
*
* @return boolean True if the current calendar needs to be synced, false otherwise.
*/
public function is_synced();
/**
* Synchronizes given events with server and returns updates.
*
* @param array List of hash arrays with event properties, must include "caldav_url" and "tag".
* @return array Tuple containing the following lists:
*
* Caldav properties for events to be created or to be updated with the keys:
* url: Event ical URL relative to calendar URL
* etag: Remote etag of the event
* local_event: The local event in case of an update.
* remote_event: The current event retrieved from caldav server.
*
* A list of event ids that are in sync.
*/
public function get_updates($events);
/**
* Creates the given event.
*
* @param array Hash array with event properties.
* @return array with updated "caldav_url" and "caldav_tag" attributes, null on error.
*/
public function create_event($event);
/**
* Updates the given event.
*
* @param array Hash array with event properties to update, must include "uid", "caldav_url" and "caldav_tag".
* @return boolean True on success, false on error, -1 if the given event/etag is not up to date.
*/
public function update_event($event);
/**
* Removes the given event.
*
* @param array Hash array with events properties, must include "caldav_url".
* @return boolean True on success, false on error.
*/
public function remove_event($event);
}

View file

@ -0,0 +1,110 @@
/**
* CalDAV Client
*
* @version @package_version@
* @author Daniel Morlock <daniel.morlock@awesome-it.de>
* @author JodliDev <jodlidev@gmail.com>
*
* Copyright (C) Awesome IT GbR <info@awesome-it.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
CREATE TABLE IF NOT EXISTS `caldav_sources` (
`source_id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`user_id` int(10) UNSIGNED NOT NULL DEFAULT '0',
`caldav_url` varchar(1024) NOT NULL,
`caldav_user` varchar(255) DEFAULT NULL,
`caldav_pass` varchar(1024) DEFAULT NULL,
PRIMARY KEY(`source_id`),
CONSTRAINT `fk_caldav_sources_user_id` FOREIGN KEY (`user_id`)
REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
CREATE TABLE IF NOT EXISTS `caldav_calendars` (
`calendar_id` int(11) UNSIGNED NOT NULL AUTO_INCREMENT,
`user_id` int(10) UNSIGNED NOT NULL DEFAULT '0',
`source_id` int(10) UNSIGNED DEFAULT NULL,
`name` varchar(255) CHARACTER SET utf8mb4 NOT NULL,
`color` varchar(8) NOT NULL,
`showalarms` tinyint(1) NOT NULL DEFAULT '1',
`caldav_tag` varchar(255) DEFAULT NULL,
`caldav_url` varchar(1024) NOT NULL,
`caldav_last_change` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`is_ical` tinyint(1) NOT NULL DEFAULT '0',
`ical_user` varchar(255) DEFAULT NULL,
`ical_pass` varchar(1024) DEFAULT NULL,
PRIMARY KEY(`calendar_id`),
INDEX `caldav_user_name_idx` (`user_id`, `name`),
CONSTRAINT `fk_caldav_calendars_user_id` FOREIGN KEY (`user_id`)
REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `fk_caldav_calendars_sources` FOREIGN KEY (`source_id`)
REFERENCES `caldav_sources`(`source_id`) ON DELETE CASCADE ON UPDATE CASCADE
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci */;
CREATE TABLE IF NOT EXISTS `caldav_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 '',
`instance` varchar(16) NOT NULL DEFAULT '',
`isexception` tinyint(1) NOT NULL DEFAULT '0',
`created` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
`changed` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
`sequence` int(1) UNSIGNED NOT NULL DEFAULT '0',
`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) CHARACTER SET utf8mb4 NOT NULL,
`description` text CHARACTER SET utf8mb4 NOT NULL,
`location` varchar(255) CHARACTER SET utf8mb4 NOT NULL DEFAULT '',
`categories` varchar(255) CHARACTER SET utf8mb4 NOT NULL DEFAULT '',
`url` 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 '0',
`sensitivity` tinyint(1) NOT NULL DEFAULT '0',
`status` varchar(32) NOT NULL DEFAULT '',
`alarms` text NULL DEFAULT NULL,
`attendees` text DEFAULT NULL,
`notifyat` datetime DEFAULT NULL,
`caldav_url` varchar(255) NOT NULL,
`caldav_tag` varchar(255) DEFAULT NULL,
`caldav_last_change` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY(`event_id`),
INDEX `caldav_uid_idx` (`uid`),
INDEX `caldav_recurrence_idx` (`recurrence_id`),
INDEX `caldav_calendar_notify_idx` (`calendar_id`,`notifyat`),
CONSTRAINT `fk_caldav_events_calendar_id` FOREIGN KEY (`calendar_id`)
REFERENCES `caldav_calendars`(`calendar_id`) ON DELETE CASCADE ON UPDATE CASCADE
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci */;
CREATE TABLE IF NOT EXISTS `caldav_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,
PRIMARY KEY(`attachment_id`),
CONSTRAINT `fk_caldav_attachments_event_id` FOREIGN KEY (`event_id`)
REFERENCES `caldav_events`(`event_id`) ON DELETE CASCADE ON UPDATE CASCADE
) /*!40000 ENGINE=INNODB */ /*!40101 CHARACTER SET utf8 COLLATE utf8_general_ci */;
REPLACE INTO `system` (`name`, `value`) VALUES ('calendar-caldav-version', '2021082400');

View file

View file

@ -0,0 +1,125 @@
/**
* CalDAV Client
* (not tested & automatically generated from mysql)
*
* @version @package_version@
* @author JodliDev <jodlidev@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
-- SQLINES LICENSE FOR EVALUATION USE ONLY
CREATE SEQUENCE IF NOT EXISTS caldav_sources_seq;
CREATE TABLE IF NOT EXISTS caldav_sources (
source_id int CHECK (source_id > 0) NOT NULL DEFAULT NEXTVAL ('caldav_sources_seq'),
user_id int CHECK (user_id > 0) NOT NULL DEFAULT '0',
caldav_url varchar(1024) NOT NULL,
caldav_user varchar(255) DEFAULT NULL,
caldav_pass varchar(1024) DEFAULT NULL,
PRIMARY KEY(source_id),
CONSTRAINT fk_caldav_sources_user_id FOREIGN KEY (user_id)
REFERENCES users(user_id) ON DELETE CASCADE ON UPDATE CASCADE
) /* SQLINES DEMO *** DB */ /* SQLINES DEMO *** ET utf8 COLLATE utf8_general_ci */;
-- SQLINES LICENSE FOR EVALUATION USE ONLY
CREATE SEQUENCE IF NOT EXISTS caldav_calendars_seq;
CREATE TABLE IF NOT EXISTS caldav_calendars (
calendar_id int CHECK (calendar_id > 0) NOT NULL DEFAULT NEXTVAL ('caldav_calendars_seq'),
user_id int CHECK (user_id > 0) NOT NULL DEFAULT '0',
source_id int CHECK (source_id > 0) DEFAULT NULL,
name varchar(255) NOT NULL,
color varchar(8) NOT NULL,
showalarms smallint NOT NULL DEFAULT '1',
caldav_tag varchar(255) DEFAULT NULL,
caldav_url varchar(1024) NOT NULL,
caldav_last_change timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP,
is_ical smallint NOT NULL DEFAULT '0',
ical_user varchar(255) DEFAULT NULL,
ical_pass varchar(1024) DEFAULT NULL,
PRIMARY KEY(calendar_id)
,
CONSTRAINT fk_caldav_calendars_user_id FOREIGN KEY (user_id)
REFERENCES users(user_id) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT fk_caldav_calendars_sources FOREIGN KEY (source_id)
REFERENCES caldav_sources(source_id) ON DELETE CASCADE ON UPDATE CASCADE
) /* SQLINES DEMO *** DB */ /* SQLINES DEMO *** ET utf8 COLLATE utf8_general_ci */;
CREATE INDEX IF NOT EXISTS caldav_user_name_idx ON caldav_calendars (user_id, name);
-- SQLINES LICENSE FOR EVALUATION USE ONLY
CREATE SEQUENCE IF NOT EXISTS caldav_events_seq;
CREATE TABLE IF NOT EXISTS caldav_events (
event_id int CHECK (event_id > 0) NOT NULL DEFAULT NEXTVAL ('caldav_events_seq'),
calendar_id int CHECK (calendar_id > 0) NOT NULL DEFAULT '0',
recurrence_id int NOT NULL DEFAULT '0',
uid varchar(255) NOT NULL DEFAULT '',
instance varchar(16) NOT NULL DEFAULT '',
isexception smallint NOT NULL DEFAULT '0',
created timestamp(0) NOT NULL DEFAULT '1000-01-01 00:00:00',
changed timestamp(0) NOT NULL DEFAULT '1000-01-01 00:00:00',
sequence int NOT NULL DEFAULT '0',
start timestamp(0) NOT NULL DEFAULT '1000-01-01 00:00:00',
"end" timestamp(0) 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 '',
url varchar(255) NOT NULL DEFAULT '',
all_day smallint NOT NULL DEFAULT '0',
free_busy smallint NOT NULL DEFAULT '0',
priority smallint NOT NULL DEFAULT '0',
sensitivity smallint NOT NULL DEFAULT '0',
status varchar(32) NOT NULL DEFAULT '',
alarms text NULL DEFAULT NULL,
attendees text DEFAULT NULL,
notifyat timestamp(0) DEFAULT NULL,
caldav_url varchar(255) NOT NULL,
caldav_tag varchar(255) DEFAULT NULL,
caldav_last_change timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY(event_id)
,
CONSTRAINT fk_caldav_events_calendar_id FOREIGN KEY (calendar_id)
REFERENCES caldav_calendars(calendar_id) ON DELETE CASCADE ON UPDATE CASCADE
) /* SQLINES DEMO *** DB */ /* SQLINES DEMO *** ET utf8 COLLATE utf8_general_ci */;
CREATE INDEX IF NOT EXISTS caldav_uid_idx ON caldav_events (uid);
CREATE INDEX IF NOT EXISTS caldav_recurrence_idx ON caldav_events (recurrence_id);
CREATE INDEX IF NOT EXISTS caldav_calendar_notify_idx ON caldav_events (calendar_id,notifyat);
-- SQLINES LICENSE FOR EVALUATION USE ONLY
CREATE SEQUENCE IF NOT EXISTS caldav_attachments_seq;
CREATE TABLE IF NOT EXISTS caldav_attachments (
attachment_id int CHECK (attachment_id > 0) NOT NULL DEFAULT NEXTVAL ('caldav_attachments_seq'),
event_id int CHECK (event_id > 0) NOT NULL DEFAULT '0',
filename varchar(255) NOT NULL DEFAULT '',
mimetype varchar(255) NOT NULL DEFAULT '',
size int NOT NULL DEFAULT '0',
data TEXT NOT NULL,
PRIMARY KEY(attachment_id),
CONSTRAINT fk_caldav_attachments_event_id FOREIGN KEY (event_id)
REFERENCES caldav_events(event_id) ON DELETE CASCADE ON UPDATE CASCADE
) /* SQLINES DEMO *** DB */ /* SQLINES DEMO *** ET utf8 COLLATE utf8_general_ci */;
INSERT INTO system (name, value) VALUES ('calendar-caldav-version', '2021082400') ON CONFLICT (name) DO UPDATE SET value = excluded.value;

View file

@ -0,0 +1,101 @@
/**
* CalDAV Client
* (not tested & automatically generated from mysql)
*
* @version @package_version@
* @author JodliDev <jodlidev@gmail.com>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
CREATE TABLE IF NOT EXISTS `caldav_sources` (
`source_id` INTEGER NOT NULL PRIMARY KEY,
`user_id` INTEGER NOT NULL DEFAULT '0',
`caldav_url` TEXT NOT NULL,
`caldav_user` TEXT DEFAULT NULL,
`caldav_pass` TEXT DEFAULT NULL,
CONSTRAINT fk_itipinvitations_user_id FOREIGN KEY (`user_id`)
REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE TABLE IF NOT EXISTS `caldav_calendars` (
`calendar_id` INTEGER NOT NULL PRIMARY KEY,
`user_id` INTEGER NOT NULL DEFAULT '0',
`source_id` INTEGER DEFAULT NULL,
`name` TEXT NOT NULL,
`color` TEXT NOT NULL,
`showalarms` tinyINTEGER NOT NULL DEFAULT '1',
`caldav_tag` TEXT DEFAULT NULL,
`caldav_url` TEXT NOT NULL,
`caldav_last_change` timestamp NOT NULL ,
`is_ical` tinyINTEGER NOT NULL DEFAULT '0',
`ical_user` TEXT DEFAULT NULL,
`ical_pass` TEXT DEFAULT NULL,
CONSTRAINT `fk_caldav_calendars_user_id` FOREIGN KEY (`user_id`)
REFERENCES `users`(`user_id`) ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT `fk_caldav_calendars_sources` FOREIGN KEY (`source_id`)
REFERENCES `caldav_sources`(`source_id`) ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE TABLE IF NOT EXISTS `caldav_events` (
`event_id` INTEGER NOT NULL PRIMARY KEY,
`calendar_id` INTEGER NOT NULL DEFAULT '0',
`recurrence_id` INTEGER NOT NULL DEFAULT '0',
`uid` TEXT NOT NULL DEFAULT '',
`instance` TEXT NOT NULL DEFAULT '',
`isexception` tinyINTEGER NOT NULL DEFAULT '0',
`created` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
`changed` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
`sequence` INTEGER NOT NULL DEFAULT '0',
`start` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
`end` datetime NOT NULL DEFAULT '1000-01-01 00:00:00',
`recurrence` TEXT DEFAULT NULL,
`title` TEXT NOT NULL,
`description` text NOT NULL,
`location` TEXT NOT NULL DEFAULT '',
`categories` TEXT NOT NULL DEFAULT '',
`url` TEXT NOT NULL DEFAULT '',
`all_day` tinyINTEGER NOT NULL DEFAULT '0',
`free_busy` tinyINTEGER NOT NULL DEFAULT '0',
`priority` tinyINTEGER NOT NULL DEFAULT '0',
`sensitivity` tinyINTEGER NOT NULL DEFAULT '0',
`status` TEXT NOT NULL DEFAULT '',
`alarms` text NULL DEFAULT NULL,
`attendees` text DEFAULT NULL,
`notifyat` datetime DEFAULT NULL,
`caldav_url` TEXT NOT NULL,
`caldav_tag` TEXT DEFAULT NULL,
`caldav_last_change` timestamp NOT NULL ,
FOREIGN KEY (`calendar_id`)
REFERENCES `caldav_calendars`(`calendar_id`) ON DELETE CASCADE ON UPDATE CASCADE
);
CREATE TABLE IF NOT EXISTS `caldav_attachments` (
`attachment_id` INTEGER NOT NULL PRIMARY KEY,
`event_id` INTEGER NOT NULL DEFAULT '0',
`filename` TEXT NOT NULL DEFAULT '',
`mimetype` TEXT NOT NULL DEFAULT '',
`size` INTEGER NOT NULL DEFAULT '0',
`data` TEXT NOT NULL,
FOREIGN KEY (`event_id`)
REFERENCES `caldav_events`(`event_id`) ON DELETE CASCADE ON UPDATE CASCADE
);
REPLACE INTO `system` (`name`, `value`) VALUES ('calendar-caldav-version', '2021082400');
CREATE INDEX caldav_user_name_idx ON caldav_calendars(user_id, name);
CREATE INDEX caldav_uid_idx ON caldav_events(uid);
CREATE INDEX caldav_recurrence_idx ON caldav_events(recurrence_id);
CREATE INDEX caldav_calendar_notify_idx ON caldav_events(calendar_id, notifyat);

View file

@ -0,0 +1,404 @@
<?php
/**
* CalDAV Client
*
* @version @package_version@
* @author Daniel Morlock <daniel.morlock@awesome-it.de>
* @author JodliDev <jodlidev@gmail.com>
*
* Copyright (C) Awesome IT GbR <info@awesome-it.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class caldav_client extends Sabre\DAV\Client
{
const CLARK_GETCTAG = '{http://calendarserver.org/ns/}getctag';
const CLARK_GETETAG = '{DAV:}getetag';
const CLARK_CALDATA = '{urn:ietf:params:xml:ns:caldav}calendar-data';
private $base_uri;
private $path;
private $libvcal;
/**
* Default constructor for CalDAV client.
*
* @param string Caldav URI to appropriate calendar.
* @param string Username for HTTP basic auth.
* @param string Password for HTTP basic auth.
*/
public function __construct($uri, $user = null, $pass = null)
{
// Include libvcalendar on demand ...
if(!class_exists("libvcalendar"))
require_once __DIR__ .'/../../../libcalendaring/libvcalendar.php';
$this->libvcal = new libvcalendar();
$tokens = parse_url($uri);
$this->base_uri = $tokens['scheme']."://".$tokens['host'].($tokens['port'] ? ":".$tokens['port'] : null);
$this->path = $tokens['path'].($tokens['query'] ? "?".$tokens['query'] : null);
$settings = array(
'baseUri' => $this->base_uri,
'authType' => Sabre\DAV\Client::AUTH_BASIC
);
$this->rc = rcmail::get_instance();
if ($user) $settings['userName'] = $user;
if ($pass) $settings['password'] = $pass;
parent::__construct($settings);
}
/**
* Fetches calendar ctag.
*
* @see http://code.google.com/p/sabredav/wiki/BuildingACalDAVClient#Retrieving_calendar_information
* @return Calendar ctag or null on error.
*/
public function get_ctag()
{
try
{
$arr = $this->propFind($this->path, array(self::CLARK_GETCTAG));
if (isset($arr[self::CLARK_GETCTAG]))
return $arr[self::CLARK_GETCTAG];
}
catch(Sabre\DAV\Exception $err)
{
rcube::raise_error(array(
'code' => $err->getHTTPCode(),
'type' => 'DAV',
'file' => $err->getFile(),
'line' => $err->getLine(),
'message' => $err->getMessage()
), true, false);
}
return null;
}
/**
* Fetches event etags and urls.
*
* @see http://code.google.com/p/sabredav/wiki/BuildingACalDAVClient#Finding_out_if_anything_changed
*
* @param array Optional list of relative event URL's to retrieve specific etags. If not specified, all etags of the current calendar are returned.
* @return array List of etag properties with keys:
* url: Event ical path relative to the calendar URL.
* etag: Current event etag.
*/
public function get_etags(array $event_urls = array())
{
$etags = array();
try
{
$arr = $this->prop_report($this->path, array(self::CLARK_GETETAG), $event_urls);
foreach ($arr as $path => $data)
{
// Some caldav server return an empty calendar as event where etag is missing. Skip this!
if($data[self::CLARK_GETETAG])
{
array_push($etags, array(
"url" => $path,
"etag" => str_replace('"', null, $data[self::CLARK_GETETAG])
));
}
}
}
catch(Sabre\DAV\Exception $err)
{
rcube::raise_error(array(
'code' => $err->getHTTPCode(),
'type' => 'DAV',
'file' => $err->getFile(),
'line' => $err->getLine(),
'message' => $err->getMessage()
), true, false);
}
return $etags;
}
/**
* Fetches calendar events.
*
* @see http://code.google.com/p/sabredav/wiki/BuildingACalDAVClient#Downloading_objects
* @param array $urls = array() Optional list of event URL's to fetch. If non is specified, all
* events from the appropriate calendar will be fetched.
* @return Array hash list that maps the events URL to the appropriate event properties.
*/
public function get_events($urls = array())
{
$events = array();
try
{
$vcals = $this->prop_report($this->path, array(
self::CLARK_GETETAG,
self::CLARK_CALDATA
), $urls);
foreach ($vcals as $path => $response)
{
$vcal = $response[self::CLARK_CALDATA];
if(!$vcal)
continue;
foreach ($this->libvcal->import($vcal) as $event) {
$events[$path] = $event;
}
}
}
catch(Sabre\DAV\Exception $err)
{
rcube::raise_error(array(
'code' => $err->getHTTPCode(),
'type' => 'DAV',
'file' => $err->getFile(),
'line' => $err->getLine(),
'message' => $err->getMessage()
), true, false);
}
return $events;
}
/**
* Does a REPORT request
*
* @param string $url
* @param array $properties List of requested properties must be specified as an array, in clark
* notation.
* @param array $event_urls If specified, a multiget report request will be initiated with the
* specified event urls.
* @param int $depth = 1 Depth should be either 0 or 1. A depth of 1 will cause a request to be
* made to the server to also return all child resources.
* @return array Hash with ics event path as key and a hash array with properties and appropriate values.
*/
public function prop_report($url, array $properties, array $event_urls = array(), $depth = 1)
{
$parent_tag = sizeof($event_urls) > 0 ? "c:calendar-multiget" : "d:propfind";
$method = sizeof($event_urls) > 0 ? 'REPORT' : 'PROPFIND';
$body = '<?xml version="1.0"?>'."\n".'<'.$parent_tag.' xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">'."\n";
$body .= ' <d:prop>'."\n";
foreach ($properties as $property)
{
list($namespace, $elementName) = Sabre\Xml\Service::parseClarkNotation($property);
if ($namespace === 'DAV:')
{
$body .= ' <d:'.$elementName.' />'."\n";
}
else
{
$body .= ' <x:'.$elementName.' xmlns:x="'.$namespace.'"/>'."\n";
}
}
$body .= ' </d:prop>'."\n";
// http://tools.ietf.org/html/rfc4791#page-90
// http://www.bedework.org/trac/bedework/wiki/Bedework/DevDocs/Filters
/*
if($start && $end)
{
$body.= ' <c:filter>'."\n".
' <c:comp-filter name="VCALENDAR">'."\n".
' <c:comp-filter name="VEVENT">'."\n".
' <c:time-range start="'.$start.'" end="'.$end.'" />'."\n".
' </c:comp-filter>'."\n".
' </c:comp-filter>'."\n".
' </c:filter>' . "\n";
}
*/
foreach ($event_urls as $event_url)
{
$body .= '<d:href>'.$event_url.'</d:href>'."\n";
}
$body .= '</'.$parent_tag.'>';
$response = $this->request($method, $url, $body, array(
'Depth' => $depth,
'Content-Type' => 'application/xml'
));
$result = $this->parseMultiStatus($response['body']);
// If depth was 0, we only return the top item
if ($depth === 0)
{
reset($result);
$result = current($result);
return isset($result[200]) ? $result[200] : array();
}
$new_result = array();
foreach ($result as $href => $status_list)
{
$new_result[$href] = isset($status_list[200]) ? $status_list[200] : array();
}
return $new_result;
}
/**
* Updates or creates a calendar event.
*
* @see http://code.google.com/p/sabredav/wiki/BuildingACalDAVClient#Updating_a_calendar_object
* @param string Event ics path for the event.
* @param array Hash array with event properties.
* @param string Current event etag to match against server data. Pass null for new events.
* @return True on success, -1 if precondition failed i.e. local etag is not up to date, false on error.
*/
public function put_event($path, $event, $etag = null)
{
try
{
$headers = array("Content-Type" => "text/calendar; charset=utf-8");
if ($etag) $headers["If-Match"] = '"'.$etag.'"';
// Temporarily disable error reporting since libvcal seems not checking array key properly.
// TODO: Remove this todo if we could ensure that those errors come not from incomplete event properties.
$err_rep = error_reporting(E_ERROR);
$vcal = $this->libvcal->export(array($event));
if (is_array($vcal))
$vcal = array_shift($vcal);
error_reporting($err_rep);
$response = $this->request('PUT', $path, $vcal, $headers);
// Following http://code.google.com/p/sabredav/wiki/BuildingACalDAVClient#Creating_a_calendar_object, the
// caldav server must not always return the new etag.
return $response["statusCode"] == 201 || // 201 (created, successfully created)
$response["statusCode"] == 204; // 204 (no content, successfully updated)
}
catch(Sabre\DAV\Exception\PreconditionFailed $err)
{
// Event tag not up to date, must be updated first ...
return -1;
}
catch(Sabre\DAV\Exception $err)
{
rcube::raise_error(array(
'code' => $err->getHTTPCode(),
'type' => 'DAV',
'file' => $err->getFile(),
'line' => $err->getLine(),
'message' => $err->getMessage()
), true, false);
}
return false;
}
/**
* Removes event of given URL.
*
* @see http://code.google.com/p/sabredav/wiki/BuildingACalDAVClient#Deleting_a_calendar_object
* @param string Event ics path for the event.
* @param string Current event etag to match against server data. Pass null to force removing the event.
* @return True on success, -1 if precondition failed i.e. local etag is not up to date, false on error.
**/
public function remove_event($path, $etag = null)
{
try
{
$headers = array("Content-Type" => "text/calendar; charset=utf-8");
if ($etag) $headers["If-Match"] = '"'.$etag.'"';
$response = $this->request('DELETE', $path, null, $headers);
return $response["statusCode"] == 204 || // 204 (no content, successfully deleted)
$response["statusCode"] == 200; // 200 (OK, successfully deleted)
}
catch(Sabre\DAV\Exception\PreconditionFailed $err)
{
// Event tag not up to date, must be updated first ...
return -1;
}
catch(Sabre\DAV\Exception $err)
{
rcube::raise_error(array(
'code' => $err->getHTTPCode(),
'type' => 'DAV',
'file' => $err->getFile(),
'line' => $err->getLine(),
'message' => $err->getMessage()
), true, false);
}
return false;
}
/**
* Make a propFind query to caldav server
* @param string $path absolute or relative URL to Resource
* @param array $props list of properties to use for the query. Properties must have clark-notation.
* @param int $depth 0 means no recurse while 1 means recurse
* @return array
*/
public function prop_find($path, $props, $depth)
{
try {
$response = $this->propFind($path, $props, $depth);
}
catch(Sabre\DAV\Exception $err)
{
rcube::raise_error(array(
'code' => $err->getHTTPCode(),
'type' => 'DAV',
'file' => $err->getFile(),
'line' => $err->getLine(),
'message' => $err->getMessage()
), true, false);
}
return $response;
}
public function create_calendar($path, $name, $color) {
$headers = array("Content-Type" => "application/xml; charset=utf-8");
$body = "<?xml version=\"1.0\" encoding=\"utf-8\" ?>
<C:mkcalendar xmlns:D=\"DAV:\" xmlns:C=\"urn:ietf:params:xml:ns:caldav\" xmlns:ical=\"http://apple.com/ns/ical/\">
<D:set>
<D:prop>
<D:displayname>$name</D:displayname>
<ical:calendar-color>#$color</ical:calendar-color>
</D:prop>
</D:set>
</C:mkcalendar>";
$response = $this->request('MKCALENDAR', $path, $body, $headers);
if($response['statusCode'] !== 201) {
rcmail::console('Could not create calendar. Response:' .print_r($response, true));
return false;
}
return true;
}
public function delete_calendar() {
$response = $this->request('DELETE', $this->base_uri .$this->path);
return $response["statusCode"] === 204 || // 204 (no content, successfully deleted)
$response["statusCode"] === 200; // 200 (OK, successfully deleted)
}
};
?>

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,254 @@
<?php
/**
* CalDAV sync for the Calendar plugin
*
* @version @package_version@
* @author Daniel Morlock <daniel.morlock@awesome-it.de>
* @author JodliDev <jodlidev@gmail.com>
*
* Copyright (C) Awesome IT GbR <info@awesome-it.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
require_once 'caldav_client.php';
require_once 'Isync.php';
class caldav_sync implements Isync
{
private $cal_id = null;
private $ctag = null;
private $username = null;
private $pass = null;
private $url = null;
public $caldav = null;
/**
* Default constructor for calendar synchronization adapter.
*
* @param array Hash array with caldav properties at least the following:
* id: Calendar ID
* caldav_url: Caldav calendar URL.
* caldav_user: Caldav http basic auth user.
* caldav_pass: Password für caldav user.
* caldav_tag: Caldav ctag for calendar.
*/
public function __construct($cal)
{
$this->cal_id = $cal["id"];
$this->url = $cal["caldav_url"];
$this->ctag = isset($cal["caldav_tag"]) ? $cal["caldav_tag"] : null;
$this->username = isset($cal["caldav_user"]) ? $cal["caldav_user"] : null;
$this->pass = isset($cal["caldav_pass"]) ? $cal["caldav_pass"] : null;
$this->caldav = new caldav_client($this->url, $this->username, $this->pass);
}
/**
* Getter for current calendar ctag.
* @return string
*/
public function get_ctag()
{
return $this->ctag;
}
/**
* Determines whether current calendar needs to be synced
* regarding the CalDAV ctag.
*
* @return boolean True if the current calendar ctag differs from the CalDAV tag which
* indicates that there are changes that must be synched. Returns false
* if the calendar is up to date, no sync necesarry.
*/
public function is_synced()
{
$is_synced = $this->ctag == $this->caldav->get_ctag() && $this->ctag;
caldav_driver::debug_log("Ctag indicates that calendar \"$this->cal_id\" ".($is_synced ? "is synced." : "needs update!"));
return $is_synced;
}
/**
* Synchronizes given events with caldav server and returns updates.
*
* @param array List of hash arrays with event properties, must include "caldav_url" and "tag".
* @return array Tuple containing the following lists:
*
* Caldav properties for events to be created or to be updated with the keys:
* url: Event ical URL relative to calendar URL
* etag: Remote etag of the event
* local_event: The local event in case of an update.
* remote_event: The current event retrieved from caldav server.
*
* A list of event ids that are in sync.
*/
public function get_updates($events)
{
$ctag = $this->caldav->get_ctag();
if($ctag)
{
$this->ctag = $ctag;
$etags = $this->caldav->get_etags();
list($updates, $synced_event_ids) = $this->_get_event_updates($events, $etags);
return array($this->_get_event_data($updates), $synced_event_ids);
}
else
{
caldav_driver::debug_log("Unkown error while fetching calendar ctag for calendar \"$this->cal_id\"!");
}
return null;
}
/**
* Determines sync status and requried updates for the given events using given list of etags.
*
* @param array List of hash arrays with event properties, must include "caldav_url" and "caldav_tag".
* @param array List of current remote etags.
* @return array Tuple containing the following lists:
*
* Caldav properties for events to be created or to be updated with the keys:
* url: Event ical URL relative to calendar URL
* etag: Remote etag of the event
* local_event: The local event in case of an update.
*
* A list of event ids that are in sync.
*/
private function _get_event_updates($events, $etags)
{
$updates = array();
$in_sync = array();
foreach ($etags as $etag)
{
$url = $etag["url"];
$etag = $etag["etag"];
$event_found = false;
foreach($events as $event)
{
if ($event["caldav_url"] == $url)
{
$event_found = true;
if ($event["caldav_tag"] != $etag)
{
caldav_driver::debug_log("Event ".$event["uid"]." needs update.");
array_push($updates, array(
"local_event" => $event,
"etag" => $etag,
"url" => $url
));
}
else
{
array_push($in_sync, $event["id"]);
}
}
}
if (!$event_found)
{
caldav_driver::debug_log("Found new event ".$url);
array_push($updates, array(
"url" => $url,
"etag" => $etag
));
}
}
return array($updates, $in_sync);
}
/**
* Fetches event data and attaches it to the given update properties.
*
* @param $updates array of update properties.
* @return array List of update properties with additional key "remote_event" containing the current caldav event.
*/
private function _get_event_data($updates)
{
$urls = array();
foreach ($updates as $update)
{
array_push($urls, $update["url"]);
}
$events = $this->caldav->get_events($urls);
foreach($updates as &$update)
{
// Attach remote events to the appropriate updates.
// Note that this assumes unique event URL's!
$url = $update["url"];
if($events[$url]) {
$update["remote_event"] = $events[$url];
$update["remote_event"]["calendar"] = $this->cal_id;
}
}
return $updates;
}
/**
* Creates the given event on the CalDAV server.
*
* @param array Hash array with event properties.
* @return array with updated "caldav_url" and "caldav_tag" attributes, null on error.
*/
public function create_event($event)
{
$props = array(
"caldav_url" => parse_url($this->url, PHP_URL_PATH)."/".$event["uid"].".ics",
"caldav_tag" => null
);
caldav_driver::debug_log("Push new event to url ".$props["caldav_url"]);
$result = $this->caldav->put_event($props["caldav_url"], $event);
if($result == false || $result < 0)
return null;
return array_merge($event, $props);
}
/**
* Updates the given event on the CalDAV server.
*
* @param array Hash array with event properties to update, must include "uid", "caldav_url" and "caldav_tag".
* @return boolean True on success, false on error, -1 if the given event/etag is not up to date.
*/
public function update_event($event)
{
caldav_driver::debug_log("Updating event uid \"".$event["uid"]."\".");
return $this->caldav->put_event($event["caldav_url"], $event, $event["caldav_tag"]);
}
/**
* Removes the given event from the caldav server.
*
* @param array Hash array with events properties, must include "caldav_url".
* @return boolean True on success, false on error.
*/
public function remove_event($event)
{
caldav_driver::debug_log("Removing event uid \"".$event["uid"]."\".");
return $this->caldav->remove_event($event["caldav_url"]);
}
};
?>

View file

@ -0,0 +1,73 @@
<?php
/**
* Encryption class
* (Copied by JodliDev from https://github.com/mstilkerich/rcmcarddav/blob/master/carddav.php)
*
* @author Jorge López Pérez <jorge@adobo.org> (original author)
* @author JodliDev <jodlidev@gmail.com>
*
*
*/
class Encryption {
private function getDesKey()
{
$rcube = rcube::get_instance();
$imap_password = $rcube->decrypt((string) $_SESSION['password']);
if ($imap_password === false || strlen($imap_password) == 0) {
throw new \Exception('No password available to use for encryption');
}
while (strlen($imap_password) < 24) {
$imap_password .= $imap_password;
}
return substr($imap_password, 0, 24);
}
/**
* Converts a password to storage format according to the password storage scheme setting.
*
* @param string $clear The password in clear text.
* @return string The password in storage format (e.g. encrypted with user password as key)
* @throws Exception
*/
public function encrypt($clear) {
// encrypted with IMAP password
$rcube = rcube::get_instance();
$imap_password = $this->getDesKey();
$rcube->config->set('carddav_des_key', $imap_password);
$crypted = $rcube->encrypt($clear, 'carddav_des_key');
// there seems to be no way to unset a preference
$rcube->config->set('carddav_des_key', '');
if ($crypted === false) {
throw new \Exception('Password encryption with user password failed');
}
return $crypted;
}
public function decrypt($crypt) {
try {
$rcube = rcube::get_instance();
$imap_password = $this->getDesKey();
$rcube->config->set('carddav_des_key', $imap_password);
$clear = $rcube->decrypt($crypt, 'carddav_des_key');
// there seems to be no way to unset a preference
$rcube->config->set('carddav_des_key', '');
if ($clear === false) {
$clear = '';
}
return $clear;
} catch (\Exception $e) {
return "";
}
}
}
?>

View file

@ -0,0 +1,163 @@
<?php
/**
* iCalendar sync for the Calendar plugin
*
* @version @package_version@
* @author Daniel Morlock <daniel.morlock@awesome-it.de>
* @author JodliDev <jodlidev@gmail.com>
*
* Copyright (C) Awesome IT GbR <info@awesome-it.de>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
class ical_sync implements Isync
{
const ACTION_NONE = 1;
const ACTION_UPDATE = 2;
const ACTION_CREATE = 4;
private $cal_id = null;
private $url = null;
private $user = null;
private $pass = null;
private $ical = null;
/**
* Default constructor for calendar synchronization adapter.
*
* @param int Calendar id.
* @param array Hash array with ical properties:
* url: Absolute URL to iCAL resource.
*/
public function __construct($props)
{
$this->ical = libcalendaring::get_ical();
$this->cal_id = $props["id"];
$this->url = $props["caldav_url"];
$this->user = isset($props["ical_user"]) ? $props["ical_user"] : null;
$this->pass = isset($props["ical_pass"]) ? $props["ical_pass"] : null;
}
/**
* Determines whether current calendar needs to be synced.
*
* @return boolean True if the current calendar needs to be synced, false otherwise.
*/
public function is_synced()
{
// No change to check that so far.
return false;
}
/**
* Fetches events from iCAL resource and returns updates.
*
* @param array List of local events.
* @return array Tuple containing the following lists:
*
* Hash list for iCAL events to be created or to be updated with the keys:
* local_event: The local event in case of an update.
* remote_event: The current event retrieved from caldav server.
*
* A list of event ids that are in sync.
*/
public function get_updates($events)
{
$context = null;
if($this->user != null && $this->pass != null)
{
$context = stream_context_create(array(
'http' => array(
'header' => "Authorization: Basic " . base64_encode("$this->user:$this->pass")
)
));
}
$vcal = file_get_contents($this->url, false, $context);
$updates = array();
$synced = array();
if($vcal !== false) {
// Hash existing events by uid.
$events_hash = array();
foreach($events as $event) {
$events_hash[$event['uid']] = $event;
}
foreach ($this->ical->import($vcal) as $remote_event) {
// Attach remote event to current calendar
$remote_event['calendar'] = $this->cal_id;
$local_event = null;
if($events_hash[$remote_event['uid']])
$local_event = $events_hash[$remote_event['uid']];
// Determine whether event don't need an update.
if($local_event && $local_event['changed'] >= $remote_event['changed']) {
array_push($synced, $local_event["id"]);
}
else if($local_event) {
array_push($updates, array('local_event' => $local_event, 'remote_event' => $remote_event, 'url' => $this->url));
}
else {
array_push($updates, array('remote_event' => $remote_event, 'url' => $this->url));
}
}
}
return array($updates, $synced);
}
/**
* Getter for current calendar ctag (only for CalDAV).
* @return string
*/
public function get_ctag() {
return 'none';
}
/**
* Creates the given event.
*
* @param array Hash array with event properties.
* @return array with updated "caldav_url" and "caldav_tag" attributes, null on error.
*/
public function create_event($event) {
return $event;
}
/**
* Updates the given event.
*
* @param array Hash array with event properties to update, must include "uid", "caldav_url" and "caldav_tag".
* @return boolean True on success, false on error, -1 if the given event/etag is not up to date.
*/
public function update_event($event) {
return false;
}
/**
* Removes the given event.
*
* @param array Hash array with events properties, must include "caldav_url".
* @return boolean True on success, false on error.
*/
public function remove_event($event) {
return false;
}
}
?>

View file

@ -362,11 +362,11 @@ abstract class calendar_driver
{ {
$valid = true; $valid = true;
if (empty($event['start']) || !is_object($event['start']) || !is_a($event['start'], 'DateTime')) { if (empty($event['start']) || !is_object($event['start']) || (!is_a($event['start'], 'DateTime') && !is_a($event['start'], 'DateTimeImmutable'))) {
$valid = false; $valid = false;
} }
if (empty($event['end']) || !is_object($event['end']) || !is_a($event['end'], 'DateTime')) { if (empty($event['end']) || !is_object($event['end']) || (!is_a($event['end'], 'DateTime') && !is_a($event['end'], 'DateTimeImmutable'))) {
$valid = false; $valid = false;
} }

View file

@ -715,6 +715,8 @@ class database_driver extends calendar_driver
'title', 'description', 'location', 'categories', 'url', 'free_busy', 'priority', 'title', 'description', 'location', 'categories', 'url', 'free_busy', 'priority',
'sensitivity', 'status', 'attendees', 'alarms', 'notifyat' 'sensitivity', 'status', 'attendees', 'alarms', 'notifyat'
); );
if(array_key_exists('notifyat', $event) && empty($event['notifyat']))
unset($event['notifyat']);
foreach ($set_cols as $col) { foreach ($set_cols as $col) {
if (!empty($event[$col]) && is_a($event[$col], 'DateTime')) { if (!empty($event[$col]) && is_a($event[$col], 'DateTime')) {

View file

@ -41,12 +41,13 @@ class resources_driver_ldap extends resources_driver
/** /**
* Fetch resource objects to be displayed for booking * Fetch resource objects to be displayed for booking
* *
* @param string $query Search query (optional) * @param string $query Search query (optional)
* @param int $num Max size of the result * @param int $num Max size of the result
* @param string $searchField Field to search with query
* *
* @return array List of resource records available for booking * @return array List of resource records available for booking
*/ */
public function load_resources($query = null, $num = 5000) public function load_resources($query = null, $num = 5000, $searchField = '*')
{ {
if (!($ldap = $this->connect())) { if (!($ldap = $this->connect())) {
return []; return [];
@ -56,7 +57,7 @@ class resources_driver_ldap extends resources_driver
$ldap->set_pagesize($num); $ldap->set_pagesize($num);
if (isset($query)) { if (isset($query)) {
$results = $ldap->search('*', $query, 0, true, true); $results = $ldap->search($searchField, $query, 0, true, true);
} }
else { else {
$results = $ldap->list_records(); $results = $ldap->list_records();

View file

@ -6,6 +6,11 @@
* *
* For translation see https://www.transifex.com/projects/p/kolab/resource/calendar/ * For translation see https://www.transifex.com/projects/p/kolab/resource/calendar/
*/ */
$labels['addsources'] = 'CalDAV Quellen hinzufügen';
$labels['deletesources'] = 'CalDAV Quellen löschen';
$labels['source_notadded_error'] = 'CalDAV Quelle konnte nicht hinzugefügt werden.';
$labels['calendar_ical_file'] = 'ics-Datei';
$labels['default_view'] = 'Standardansicht'; $labels['default_view'] = 'Standardansicht';
$labels['time_format'] = 'Zeitformatierung'; $labels['time_format'] = 'Zeitformatierung';
$labels['timeslots'] = 'Zeitfenster pro Stunde'; $labels['timeslots'] = 'Zeitfenster pro Stunde';

View file

@ -6,6 +6,11 @@
* *
* For translation see https://www.transifex.com/projects/p/kolab/resource/calendar/ * For translation see https://www.transifex.com/projects/p/kolab/resource/calendar/
*/ */
$labels['addsources'] = 'CalDAV Quellen hinzufügen';
$labels['deletesources'] = 'CalDAV Quellen löschen';
$labels['source_notadded_error'] = 'CalDAV Quelle konnte nicht hinzugefügt werden.';
$labels['calendar_ical_file'] = 'ics-Datei';
$labels['default_view'] = 'Standardansicht'; $labels['default_view'] = 'Standardansicht';
$labels['time_format'] = 'Zeitformatierung'; $labels['time_format'] = 'Zeitformatierung';
$labels['timeslots'] = 'Abschnitte pro Stunde'; $labels['timeslots'] = 'Abschnitte pro Stunde';

View file

@ -6,6 +6,11 @@
* *
* For translation see https://www.transifex.com/projects/p/kolab/resource/calendar/ * For translation see https://www.transifex.com/projects/p/kolab/resource/calendar/
*/ */
$labels['addsources'] = 'CalDAV Quellen hinzufügen';
$labels['deletesources'] = 'CalDAV Quellen löschen';
$labels['source_notadded_error'] = 'CalDAV Quelle konnte nicht hinzugefügt werden.';
$labels['calendar_ical_file'] = 'ics-Datei';
$labels['default_view'] = 'Standardansicht'; $labels['default_view'] = 'Standardansicht';
$labels['time_format'] = 'Zeitformatierung'; $labels['time_format'] = 'Zeitformatierung';
$labels['timeslots'] = 'Zeitfenster pro Stunde'; $labels['timeslots'] = 'Zeitfenster pro Stunde';

View file

@ -10,6 +10,12 @@
$labels = array(); $labels = array();
//caldav driver
$labels['addsources'] = 'Add CalDAV sources';
$labels['deletesources'] = 'Delete CalDAV sources';
$labels['source_notadded_error'] = 'CalDAV source could not be added.';
$labels['calendar_ical_file'] = 'ics file';
// preferences // preferences
$labels['default_view'] = 'Default view'; $labels['default_view'] = 'Default view';
$labels['time_format'] = 'Time format'; $labels['time_format'] = 'Time format';
@ -207,6 +213,10 @@ $labels['itipmailbodycancel'] = "\$sender has rejected your participation in the
$labels['itipmailbodydelegated'] = "\$sender has delegated the participation in the following event:\n\n*\$title*\n\nWhen: \$date"; $labels['itipmailbodydelegated'] = "\$sender has delegated the participation in the following event:\n\n*\$title*\n\nWhen: \$date";
$labels['itipmailbodydelegatedto'] = "\$sender has delegated the participation in the following event to you:\n\n*\$title*\n\nWhen: \$date"; $labels['itipmailbodydelegatedto'] = "\$sender has delegated the participation in the following event to you:\n\n*\$title*\n\nWhen: \$date";
$labels['itipmailbodyresourceaccepted'] = "\$sender has accepted the following resource booking:\n\n*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees";
$labels['itipmailbodyresourcetentative'] = "\$sender has tentatively accepted the following resource booking:\n\n*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees";
$labels['itipmailbodyresourcedeclined'] = "\$sender has declined the the following resource booking:\n\n*\$title*\n\nWhen: \$date\n\nInvitees: \$attendees";
$labels['itipdeclineevent'] = 'Do you want to decline your invitation to this event?'; $labels['itipdeclineevent'] = 'Do you want to decline your invitation to this event?';
$labels['declinedeleteconfirm'] = 'Do you also want to delete this declined event from your calendar?'; $labels['declinedeleteconfirm'] = 'Do you also want to delete this declined event from your calendar?';
$labels['itipcomment'] = 'Invitation/notification comment'; $labels['itipcomment'] = 'Invitation/notification comment';

View file

@ -141,6 +141,10 @@
<div id="calendaractions-menu" class="popupmenu"> <div id="calendaractions-menu" class="popupmenu">
<h3 id="aria-label-calendaroptions" class="voice"><roundcube:label name="calendar.calendaractions" /></h3> <h3 id="aria-label-calendaroptions" class="voice"><roundcube:label name="calendar.calendaractions" /></h3>
<ul class="menu listing" role="menu" aria-labelledby="aria-label-calendaroptions"> <ul class="menu listing" role="menu" aria-labelledby="aria-label-calendaroptions">
<roundcube:if condition="env:calendar_driver == 'caldav'" />
<roundcube:button type="link-menuitem" command="calendar-sources-new" label="calendar.addsources" class="create" />
<roundcube:button type="link-menuitem" command="calendar-sources-delete" label="calendar.deletesources" class="delete" />
<roundcube:endif />
<roundcube:button type="link-menuitem" command="calendar-create" label="calendar.addcalendar" class="create disabled" classAct="create active" /> <roundcube:button type="link-menuitem" command="calendar-create" label="calendar.addcalendar" class="create disabled" classAct="create active" />
<roundcube:button type="link-menuitem" command="calendar-edit" label="calendar.editcalendar" class="edit disabled" classAct="edit active" /> <roundcube:button type="link-menuitem" command="calendar-edit" label="calendar.editcalendar" class="edit disabled" classAct="edit active" />
<roundcube:button type="link-menuitem" command="calendar-delete" label="calendar.deletecalendar" class="delete disabled" classAct="delete active" /> <roundcube:button type="link-menuitem" command="calendar-delete" label="calendar.deletecalendar" class="delete disabled" classAct="delete active" />

View file

@ -68,6 +68,10 @@
<div id="calendaroptionsmenu" class="popupmenu" aria-hidden="true"> <div id="calendaroptionsmenu" class="popupmenu" aria-hidden="true">
<h3 id="aria-label-calendaroptions" class="voice"><roundcube:label name="calendar.calendaractions" /></h3> <h3 id="aria-label-calendaroptions" class="voice"><roundcube:label name="calendar.calendaractions" /></h3>
<ul id="calendaroptionsmenu-menu" class="toolbarmenu" role="menu" aria-labelledby="aria-label-calendaroptions"> <ul id="calendaroptionsmenu-menu" class="toolbarmenu" role="menu" aria-labelledby="aria-label-calendaroptions">
<roundcube:if condition="env:calendar_driver == 'caldav'" />
<li role="menuitem"><roundcube:button type="link" command="calendar-sources-new" label="calendar.addsources" class="active" /></li>
<li role="menuitem"><roundcube:button type="link" command="calendar-sources-delete" label="calendar.deletesources" class="active" /></li>
<roundcube:endif />
<li role="menuitem"><roundcube:button type="link" command="calendar-edit" label="calendar.edit" classAct="active" /></li> <li role="menuitem"><roundcube:button type="link" command="calendar-edit" label="calendar.edit" classAct="active" /></li>
<li role="menuitem"><roundcube:button type="link" command="calendar-delete" label="delete" classAct="active" /></li> <li role="menuitem"><roundcube:button type="link" command="calendar-delete" label="delete" classAct="active" /></li>
<roundcube:if condition="env:calendar_driver == 'kolab'" /> <roundcube:if condition="env:calendar_driver == 'kolab'" />