Compare commits

..

37 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
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
17 changed files with 332 additions and 220 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

@ -1251,23 +1251,31 @@ $("#rcmfd_new_category").keypress(function(event) {
$noreply = intval($noreply) || $status == 'needs-action' || $itip_sending === 0;
$reload = $event['calendar'] != $ev['calendar'] || !empty($event['recurrence']) ? 2 : 1;
$emails = $this->get_user_emails();
$ownedResourceEmails = $this->owned_resources_emails();
$organizer = null;
$resourceConfirmation = false;
foreach ($event['attendees'] as $i => $attendee) {
if ($attendee['role'] == 'ORGANIZER') {
$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'];
}
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) {
$itip = $this->load_itip();
$itip->set_sender_email($reply_sender);
$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'];
$msg = $this->gettext(['name' => 'sentresponseto', 'vars' => ['mailto' => $mailto]]);
@ -2018,10 +2026,12 @@ $("#rcmfd_new_category").keypress(function(event) {
}
$identity['emails'][] = $this->rc->user->get_username();
$identity['ownedResources'] = $this->owned_resources_emails();
$settings['identity'] = [
'name' => $identity['name'],
'email' => strtolower($identity['email']),
'emails' => ';' . strtolower(join(';', $identity['emails']))
'emails' => ';' . strtolower(join(';', $identity['emails'])),
'ownedResources' => ';' . strtolower(join(';', $identity['ownedResources']))
];
}
@ -2402,7 +2412,7 @@ $("#rcmfd_new_category").keypress(function(event) {
}
// set new organizer identity
if ($organizer !== false && $identity) {
if ($organizer !== false && !empty($identity)) {
$event['attendees'][$organizer]['name'] = $identity['name'];
$event['attendees'][$organizer]['email'] = $identity['email'];
}
@ -2412,7 +2422,7 @@ $("#rcmfd_new_category").keypress(function(event) {
unset($event['attendees'][$owner]['rsvp']);
}
// fallback to the selected identity
else if ($organizer === false && $identity) {
else if ($organizer === false && !empty($identity)) {
$event['attendees'][] = [
'role' => 'ORGANIZER',
'name' => $identity['name'],
@ -2879,6 +2889,21 @@ $("#rcmfd_new_category").keypress(function(event) {
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 ****/
/**
@ -3160,10 +3185,16 @@ $("#rcmfd_new_category").keypress(function(event) {
*/
private function mail_agenda_event_row($event, $class = '')
{
$time = !empty($event['allday']) ? $this->gettext('all-day') :
$this->rc->format_date($event['start'], $this->rc->config->get('time_format'))
. ' - ' .
$this->rc->format_date($event['end'], $this->rc->config->get('time_format'));
if (!empty($event['allday'])) {
$time = $this->gettext('all-day');
}
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'])),
html::span('event-date', $time)
@ -3228,7 +3259,7 @@ $("#rcmfd_new_category").keypress(function(event) {
// get prepared inline UI for this event object
if ($ical_objects->method) {
$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'));
// prepare a small agenda preview to be filled with actual event data on async request
@ -3425,39 +3456,38 @@ $("#rcmfd_new_category").keypress(function(event) {
// only update attendee status
if ($event['_method'] == 'REPLY') {
// try to identify the attendee using the email sender address
$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;
}
}
$existing_attendee_index = -1;
$event_attendee = null;
$update_attendees = [];
foreach ($event['attendees'] as $attendee) {
if ($this->itip->compare_email($attendee['email'], $event['_sender'], $event['_sender_utf'])) {
$event_attendee = $attendee;
$update_attendees[] = $attendee;
$metadata['fallback'] = $attendee['status'];
$metadata['attendee'] = $attendee['email'];
$metadata['rsvp'] = !empty($attendee['rsvp']) || $attendee['role'] != 'NON-PARTICIPANT';
if ($attendee = $this->itip->find_reply_attendee($event)) {
$event_attendee = $attendee;
$update_attendees[] = $attendee;
$metadata['fallback'] = $attendee['status'];
$metadata['attendee'] = $attendee['email'];
$metadata['rsvp'] = !empty($attendee['rsvp']) || $attendee['role'] != 'NON-PARTICIPANT';
if ($attendee['status'] != 'DELEGATED') {
break;
$existing_attendee_emails = [];
// 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'])
&& $this->itip->compare_email($attendee['delegated-from'], $event['_sender'], $event['_sender_utf'])
) {
$update_attendees[] = $attendee;
if (!in_array_nocase($attendee['email'], $existing_attendee_emails)) {
$existing['attendees'][] = $attendee;
if ($attendee['status'] == 'DELEGATED') {
//Also find and copy the delegatee
$delegatee_email = $attendee['email'];
$delegatees = array_filter($event['attendees'], function($attendee) use ($delegatee_email){ return $attendee['role'] != 'ORGANIZER' && $this->itip->compare_email($attendee['delegated-from'], $delegatee_email); });
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;
}
}
}
}
@ -3475,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
if ($existing_attendee >= 0 && $event_attendee) {
$existing['attendees'][$existing_attendee] = $event_attendee;
if ($existing_attendee_index >= 0 && $event_attendee) {
$existing['attendees'][$existing_attendee_index] = $event_attendee;
$success = $this->driver->update_attendees($existing, $update_attendees);
}
// update the entire attendees block

View file

@ -459,7 +459,7 @@ function rcube_calendar_ui(settings)
for (var j=0; j < num_attendees; j++) {
data = event.attendees[j];
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();
if (data.status == 'NEEDS-ACTION' || data.status == 'TENTATIVE' || data.rsvp)
rsvp = mystatus;
@ -2379,6 +2379,14 @@ function rcube_calendar_ui(settings)
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
var event_rsvp = function(response, delegate, replymode, event)
{
@ -2413,7 +2421,8 @@ function rcube_calendar_ui(settings)
attendees = [];
for (var data, i=0; i < me.selected_event.attendees.length; 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.rsvp = 0; // unset RSVP flag
@ -2953,7 +2962,7 @@ function rcube_calendar_ui(settings)
$dialog = $('<iframe>').attr('src', rcmail.url('calendar', params)).on('load', function() {
var contents = $(this).contents();
contents.find('#calendar-name')
.prop('disabled', !calendar.editable)
.prop('disabled', !calendar.editable && !calendar.editable_name)
.val(calendar.editname || calendar.name)
.select();
contents.find('#calendar-color')
@ -3607,7 +3616,7 @@ function rcube_calendar_ui(settings)
if (node && node.id && me.calendars[node.id]) {
me.select_calendar(node.id, true);
rcmail.enable_command('calendar-edit', 'calendar-showurl', 'calendar-showfburl', true);
rcmail.enable_command('calendar-delete', me.calendars[node.id].editable);
rcmail.enable_command('calendar-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);
}
});
@ -4226,7 +4235,7 @@ window.rcmail && rcmail.addEventListener('init', function(evt) {
rcmail.register_command('print', function(){ cal.print_calendars(); }, true);
// configure list operations
rcmail.register_command('calendar-sources-add', cal.calendar_new_source, true);
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-edit', function(){ cal.calendar_edit_dialog(cal.calendars[cal.selected_calendar]); }, false);

View file

@ -4,7 +4,7 @@
"description": "Calendar plugin",
"homepage": "https://git.kolab.org/diffusion/RPK/",
"license": "AGPLv3",
"version": "3.5.7",
"version": "3.5.11",
"authors": [
{
"name": "Thomas Bruederli",
@ -30,7 +30,7 @@
"require": {
"php": ">=5.5",
"roundcube/plugin-installer": ">=0.1.3",
"jodlidev/libcalendaring": ">=3.4.0",
"jodlidev/libcalendaring": "dev-master",
"kolab/libkolab": ">=3.4.0",
"sabre/dav": ">=4.1.5"
},

View file

@ -30,13 +30,13 @@ $config['calendar_driver'] = "caldav";
// Enable debugging output for iCAL/CalDAV drivers
$config['calendar_caldav_debug'] = false;
$config['calendar_ical_debug'] = false;
// default calendar view (agendaDay, agendaWeek, month)
$config['calendar_default_view'] = "agendaWeek";
// show a birthdays calendar from the user's address book(s)
$config['calendar_contact_birthdays'] = false;
$config['birthday_calendar'] = array('color' => 'fffb00');
// timeslots per hour (1, 2, 3, 4, 6)
$config['calendar_timeslots'] = 2;
@ -143,6 +143,20 @@ $config['kolab_invitation_calendars'] = false;
// %i - Calendar UUID
// $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).
// Leave empty or commented to disable resources support.
// $config['calendar_resources_driver'] = 'ldap';

View file

@ -37,8 +37,8 @@ CREATE TABLE IF NOT EXISTS `caldav_sources` (
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 NOT NULL DEFAULT '0',
`name` varchar(255) NOT NULL,
`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',
@ -55,7 +55,7 @@ CREATE TABLE IF NOT EXISTS `caldav_calendars` (
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 utf8 COLLATE utf8_general_ci */;
) /*!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,
@ -70,10 +70,10 @@ CREATE TABLE IF NOT EXISTS `caldav_events` (
`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 '',
`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',
@ -94,7 +94,7 @@ CREATE TABLE IF NOT EXISTS `caldav_events` (
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 utf8 COLLATE utf8_general_ci */;
) /*!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,

View file

@ -20,7 +20,7 @@
*/
-- SQLINES LICENSE FOR EVALUATION USE ONLY
CREATE SEQUENCE caldav_sources_seq;
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'),
@ -36,12 +36,12 @@ CREATE TABLE IF NOT EXISTS caldav_sources (
) /* SQLINES DEMO *** DB */ /* SQLINES DEMO *** ET utf8 COLLATE utf8_general_ci */;
-- SQLINES LICENSE FOR EVALUATION USE ONLY
CREATE SEQUENCE caldav_calendars_seq;
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) 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',
@ -61,23 +61,23 @@ CREATE TABLE IF NOT EXISTS caldav_calendars (
REFERENCES caldav_sources(source_id) ON DELETE CASCADE ON UPDATE CASCADE
) /* SQLINES DEMO *** DB */ /* SQLINES DEMO *** ET utf8 COLLATE utf8_general_ci */;
CREATE INDEX caldav_user_name_idx ON caldav_calendars (user_id, name);
CREATE INDEX IF NOT EXISTS caldav_user_name_idx ON caldav_calendars (user_id, name);
-- SQLINES LICENSE FOR EVALUATION USE ONLY
CREATE SEQUENCE caldav_events_seq;
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 CHECK (recurrence_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 CHECK (sequence > 0) NOT NULL DEFAULT '0',
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',
"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,
@ -103,12 +103,12 @@ CREATE TABLE IF NOT EXISTS caldav_events (
REFERENCES caldav_calendars(calendar_id) ON DELETE CASCADE ON UPDATE CASCADE
) /* SQLINES DEMO *** DB */ /* SQLINES DEMO *** ET utf8 COLLATE utf8_general_ci */;
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);
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 caldav_attachments_seq;
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'),
@ -122,4 +122,4 @@ CREATE TABLE IF NOT EXISTS caldav_attachments (
REFERENCES caldav_events(event_id) ON DELETE CASCADE ON UPDATE CASCADE
) /* SQLINES DEMO *** DB */ /* SQLINES DEMO *** ET utf8 COLLATE utf8_general_ci */;
REPLACE INTO `system` (name, value) SELECT ('calendar-caldav-version', '2021082400');
INSERT INTO system (name, value) VALUES ('calendar-caldav-version', '2021082400') ON CONFLICT (name) DO UPDATE SET value = excluded.value;

View file

@ -33,7 +33,7 @@ CREATE TABLE IF NOT EXISTS `caldav_sources` (
CREATE TABLE IF NOT EXISTS `caldav_calendars` (
`calendar_id` INTEGER NOT NULL PRIMARY KEY,
`user_id` INTEGER NOT NULL DEFAULT '0',
`source_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',

View file

@ -388,7 +388,11 @@ class caldav_client extends Sabre\DAV\Client
</C:mkcalendar>";
$response = $this->request('MKCALENDAR', $path, $body, $headers);
return $response["statusCode"] === 201;
if($response['statusCode'] !== 201) {
rcmail::console('Could not create calendar. Response:' .print_r($response, true));
return false;
}
return true;
}
public function delete_calendar() {

View file

@ -101,9 +101,41 @@ class caldav_driver extends calendar_driver
if(self::$debug === null)
self::$debug = $this->rc->config->get('calendar_caldav_debug', False);
$this->_setup_preinstalled_sources();
$this->_read_calendars();
}
/**
* Setup preinstalled sources defined in config file
*/
protected function _setup_preinstalled_sources()
{
$preinstalled_sources = $this->rc->config->get('calendar_caldav_preinstalled_sources', FALSE);
if ($preinstalled_sources && is_array($preinstalled_sources)) {
$username = $this->rc->get_user_name();
$password = $this->rc->get_user_password();
foreach ($preinstalled_sources as $cal){
$url = $cal['caldav_url'];
$user = $cal['caldav_user'];
$pass = $cal['caldav_pass'];
$url = str_replace('%u', $username, $url);
$user = str_replace('%u', $username, $user);
$pass = str_replace('%p', $password, $pass);
$cal['caldav_url'] = $url;
$cal['caldav_user'] = $user;
$cal['caldav_pass'] = $pass;
if (!$this->create_source($cal)) {
$error_msg = 'Unable to add default calendars' . ($this->last_error ? ': ' . $this->last_error :'');
$this->rc->output->show_message($error_msg, 'error');
}
}
}
}
/**
* Read available calendars for the current user and store them internally
*/
@ -141,7 +173,6 @@ class caldav_driver extends calendar_driver
if($arr['is_ical']) {
$this->sync_clients[$arr['id']] = new ical_sync($arr);
$arr["readonly"] = true;
$arr["editable"] = false;
$arr["deletable"] = true;
$arr["editable_name"] = true;
@ -186,16 +217,16 @@ class caldav_driver extends calendar_driver
if (empty($active) || !in_array($id, $hidden)) {
$calendars[$id] = array(
'id' => $id,
'name' => $this->cal->gettext('birthdays'),
'listname' => $this->cal->gettext('birthdays'),
'color' => $prefs['color'],
'showalarms' => (bool)$this->rc->config->get('calendar_birthdays_alarm_type'),
'active' => !in_array($id, $hidden),
'group' => 'x-birthdays',
'editable' => false,
'default' => false,
'children' => false,
'id' => $id,
'name' => $this->cal->gettext('birthdays'),
'listname' => $this->cal->gettext('birthdays'),
'color' => $prefs['color'],
'showalarms' => (bool)$this->rc->config->get('calendar_birthdays_alarm_type'),
'active' => !in_array($id, $hidden),
'group' => 'x-birthdays',
'editable' => false,
'default' => false,
'children' => false,
);
}
}
@ -241,7 +272,7 @@ class caldav_driver extends calendar_driver
$source['caldav_pass'] = $this->_decrypt_pass($source['caldav_pass']);
$server_url = self::_encode_url($source['caldav_url']);
$server_path = parse_url($server_url, PHP_URL_PATH);
$server_path = rtrim(parse_url($server_url, PHP_URL_PATH), '/');
$calId = $this->cal->generate_uid();
$path = "/calendars/$source[caldav_user]/$calId";
@ -270,7 +301,7 @@ class caldav_driver extends calendar_driver
if($this->rc->db->affected_rows($result)) continue;
$cal = array(
'caldav_url' => self::_encode_url($calendar['href']),
'caldav_url' => $calendar['href'],
'name' => $calendar['name'],
'color' => $calendar['color']
);
@ -302,15 +333,36 @@ class caldav_driver extends calendar_driver
public function create_source($source)
{
$source['caldav_url'] = self::_encode_url($source['caldav_url']);
try {
// Re-discover all existing calendars systematically
try {
$calendars = $this->_autodiscover_calendars($source);
}
}
catch(Exception $e) {
self::debug_log($e);
$this->rc->output->show_message($this->cal->gettext('source_notadded_error'), 'error');
return false;
}
// Remove local data associated with deprecated calendars
$caldav_urls = array_column($calendars, 'href');
$query = $this->rc->db->query(
"DELETE FROM " . $this->db_calendars . " WHERE user_id=? AND caldav_url NOT IN ('" . implode("','", $caldav_urls) . "')",
$this->rc->user->ID);
$this->rc->db->affected_rows($query);
// Skip update if the set of available calendars matches
$result = $this->rc->db->query(
"SELECT calendar_id FROM " . $this->db_calendars . " WHERE user_id=? AND caldav_url LIKE ?",
$this->rc->user->ID, $source['caldav_url'] . '%');
$count_cur = $this->rc->db->num_rows($result);
$count_avail = count($calendars);
if ($count_cur == $count_avail)
{
self::debug_log("Skip source update.");
return true;
}
if(count($calendars)) {
$pass = isset($source['caldav_pass']) ? $this->_encrypt_pass($source['caldav_pass']) : null;
$db_source_result = $this->rc->db->query(
@ -360,11 +412,11 @@ class caldav_driver extends calendar_driver
$this->rc->user->ID,
$source ? $source['source_id'] : null,
$prop['name'],
$prop['color'],
isset($prop['color']) ? $prop['color'] : '#cc0000',
$prop['showalarms']?1:0,
$prop['caldav_url'],
isset($prop["caldav_tag"]) ? $prop["caldav_tag"] : null,
!!$prop['is_ical']
$prop['is_ical']?1:0
);
if ($result)
@ -503,6 +555,11 @@ class caldav_driver extends calendar_driver
{
//$event = $this->_save_preprocess($event);
//TODO: proper 4 byte character (eg emoticons) handling
//utf8 in mysql only supports 3 byte characters, so this throws an error if there are emoticons in the description.
//For now we just remove them. But instead of removing, we should prepare the database for them (by using utf8mb4)
$desc = preg_replace('/[\xF0-\xF7].../s', '', strval($event['description']));
$this->rc->db->query(sprintf(
"INSERT INTO " . $this->db_events . "
(calendar_id, created, changed, uid, recurrence_id, instance, isexception, %s, %s, all_day, recurrence,
@ -524,7 +581,7 @@ class caldav_driver extends calendar_driver
intval($event['all_day']),
$event['_recurrence'],
strval($event['title']),
strval($event['description']),
$desc,
strval($event['location']),
join(',', (array)$event['categories']),
strval($event['url']),
@ -880,19 +937,20 @@ class caldav_driver extends calendar_driver
}
// compose vcalendar-style recurrencue rule from structured data
$rrule = $event['recurrence'] ? libcalendaring::to_rrule($event['recurrence']) : '';
$rrule = !empty($event['recurrence']) ? libcalendaring::to_rrule($event['recurrence']) : '';
$sensitivity = strtolower($event['sensitivity']);
$free_busy = strtolower($event['free_busy']);
$event['_recurrence'] = rtrim($rrule, ';');
$event['free_busy'] = intval($this->free_busy_map[strtolower($event['free_busy'])]);
$event['sensitivity'] = intval($this->sensitivity_map[strtolower($event['sensitivity'])]);
$event['free_busy'] = isset($this->free_busy_map[$free_busy]) ? $this->free_busy_map[$free_busy] : null;
$event['sensitivity'] = isset($this->sensitivity_map[$sensitivity]) ? $this->sensitivity_map[$sensitivity] : null;
$event['all_day'] = !empty($event['allday']) ? 1 : 0;
if ($event['free_busy'] == 'tentative') {
$event['status'] = 'TENTATIVE';
}
if (isset($event['allday'])) {
$event['all_day'] = $event['allday'] ? 1 : 0;
}
// compute absolute time to notify the user
$event['notifyat'] = $this->_get_notification($event);
@ -940,8 +998,15 @@ class caldav_driver extends calendar_driver
$sql_set[] = $this->rc->db->quote_identifier($col) . '=' . $this->rc->db->quote($event[$col]->format(self::DB_DATE_FORMAT));
else if (is_array($event[$col]))
$sql_set[] = $this->rc->db->quote_identifier($col) . '=' . $this->rc->db->quote(join(',', $event[$col]));
else if (array_key_exists($col, $event))
$sql_set[] = $this->rc->db->quote_identifier($col) . '=' . $this->rc->db->quote($event[$col]);
else if (array_key_exists($col, $event) && is_null($event[$col]))
$sql_set[] = $this->rc->db->quote_identifier($col) . '=NULL';
else if (array_key_exists($col, $event)) {
//TODO: proper 4 byte character (eg emoticons) handling
//utf8 in mysql only supports 3 byte characters, so this throws an error if there are emoticons in the description.
//For now we just remove them. But instead of removing, we should prepare the database for them (by using utf8mb4)
$text = preg_replace('/[\xF0-\xF7].../s', '', $event[$col]);
$sql_set[] = $this->rc->db->quote_identifier($col) . '=' . $this->rc->db->quote($text);
}
}
if ($event['_recurrence'])
@ -1897,11 +1962,11 @@ class caldav_driver extends calendar_driver
'SELECT source_id, caldav_url FROM '.$this->db_sources .' WHERE user_id = ?',
$this->rc->user->ID
);
$sources_exist = $this->rc->db->num_rows($result);
if($this->rc->db->num_rows($result)) {
$is_ical = new html_checkbox( array(
'name' => "is_ical",
'value' => 1,
'onload' => 'alert(123)',
'onclick' => '
if(this.checked) {
$("#ical_url").removeClass("hidden");
@ -1913,7 +1978,7 @@ else {
}'
));
$formfields['is_ical'] = array(
'label' => $this->cal->gettext('calendar_is_ical'),
'label' => $this->cal->gettext('calendar_ical_file'),
'value' => $is_ical->show(null),
'class' => 'hidden'
);
@ -1924,7 +1989,6 @@ else {
'class' => 'hidden'
));
$caldav_url = new html_select([
'name' => 'source_id',
'id' => 'caldav_url'
@ -1938,8 +2002,19 @@ else {
);
}
else {
$this->rc->output->show_message($this->cal->gettext('nosources_error'), 'error');
return null;
$ical_url = new html_inputfield( array(
'name' => 'ical_url',
'size' => 20,
));
$formfields['url'] = array(
'label' => $this->cal->gettext('calendar_ical_file'),
'value' => $ical_url->show(null),
);
$enable_ics = new html_hiddenfield(['name' => 'is_ical', 'value' => 1]);
$formfields['hidden'] = array(
'label' => ' ',
'value' =>$enable_ics->show(null),
);
}
}
@ -2047,14 +2122,13 @@ else {
preg_match('/#(.)(.)(.)/', $value, $matches);
$color = $matches[1] .$matches[1] .
$matches[2] .$matches[2] .
$matches[3] .$matches[3] .
'ff';
$matches[3] .$matches[3];
break;
case 7:
$color = substr($value, 1) . 'ff';
$color = substr($value, 1);
break;
case 9:
$color = substr($value, 1);
$color = substr($value, 1, 6);
break;
}
}
@ -2156,6 +2230,17 @@ else {
foreach($updates as $update)
{
if($update['remote_event']['allday'])
{
//caldav has exclusive end dates set to midnight of the next day.
//But we need it inclusive, so we just reduce it by an hour:
$old = $update['remote_event']['end'];
$new = new DateTime('now', $old->getTimezone());
$new->setTimestamp($old->getTimestamp() - 3600);
$update['remote_event']['end'] = $new;
}
// local event -> update event
if(isset($update["local_event"]))
{
@ -2247,7 +2332,7 @@ else {
{
switch ($this->rc->db->db_provider) {
case 'postgres':
return "EXTRACT (EPOCH FROM $field)";
return "EXTRACT (EPOCH FROM $field::timestamp without time zone)";
default:
return "UNIX_TIMESTAMP($field)";
}

View file

@ -715,6 +715,8 @@ class database_driver extends calendar_driver
'title', 'description', 'location', 'categories', 'url', 'free_busy', 'priority',
'sensitivity', 'status', 'attendees', 'alarms', 'notifyat'
);
if(array_key_exists('notifyat', $event) && empty($event['notifyat']))
unset($event['notifyat']);
foreach ($set_cols as $col) {
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
*
* @param string $query Search query (optional)
* @param int $num Max size of the result
* @param string $query Search query (optional)
* @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
*/
public function load_resources($query = null, $num = 5000)
public function load_resources($query = null, $num = 5000, $searchField = '*')
{
if (!($ldap = $this->connect())) {
return [];
@ -56,7 +57,7 @@ class resources_driver_ldap extends resources_driver
$ldap->set_pagesize($num);
if (isset($query)) {
$results = $ldap->search('*', $query, 0, true, true);
$results = $ldap->search($searchField, $query, 0, true, true);
}
else {
$results = $ldap->list_records();

View file

@ -8,9 +8,8 @@
*/
$labels['addsources'] = 'CalDAV Quellen hinzufügen';
$labels['deletesources'] = 'CalDAV Quellen löschen';
$labels['nosources_error'] = 'Keine CalDAV Quellen vorhanden.';
$labels['source_notadded_error'] = 'CalDAV Quelle konnte nicht hinzugefügt werden.';
$labels['calendar_is_ical'] = 'Ist ics-Datei';
$labels['calendar_ical_file'] = 'ics-Datei';
$labels['default_view'] = 'Standardansicht';
$labels['time_format'] = 'Zeitformatierung';

View file

@ -8,9 +8,8 @@
*/
$labels['addsources'] = 'CalDAV Quellen hinzufügen';
$labels['deletesources'] = 'CalDAV Quellen löschen';
$labels['nosources_error'] = 'Keine CalDAV Quellen vorhanden.';
$labels['source_notadded_error'] = 'CalDAV Quelle konnte nicht hinzugefügt werden.';
$labels['calendar_is_ical'] = 'Ist ics-Datei';
$labels['calendar_ical_file'] = 'ics-Datei';
$labels['default_view'] = 'Standardansicht';
$labels['time_format'] = 'Zeitformatierung';

View file

@ -8,9 +8,8 @@
*/
$labels['addsources'] = 'CalDAV Quellen hinzufügen';
$labels['deletesources'] = 'CalDAV Quellen löschen';
$labels['nosources_error'] = 'Keine CalDAV Quellen vorhanden.';
$labels['source_notadded_error'] = 'CalDAV Quelle konnte nicht hinzugefügt werden.';
$labels['calendar_is_ical'] = 'Ist ics-Datei';
$labels['calendar_ical_file'] = 'ics-Datei';
$labels['default_view'] = 'Standardansicht';
$labels['time_format'] = 'Zeitformatierung';

View file

@ -13,9 +13,8 @@ $labels = array();
//caldav driver
$labels['addsources'] = 'Add CalDAV sources';
$labels['deletesources'] = 'Delete CalDAV sources';
$labels['nosources_error'] = 'No CalDAV sources available.';
$labels['source_notadded_error'] = 'CalDAV source could not be added.';
$labels['calendar_is_ical'] = 'Is ics file';
$labels['calendar_ical_file'] = 'ics file';
// preferences
$labels['default_view'] = 'Default view';
@ -214,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['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['declinedeleteconfirm'] = 'Do you also want to delete this declined event from your calendar?';
$labels['itipcomment'] = 'Invitation/notification comment';