aboutsummaryrefslogtreecommitdiffstats
path: root/apps/dav
diff options
context:
space:
mode:
Diffstat (limited to 'apps/dav')
-rw-r--r--apps/dav/composer/composer/autoload_classmap.php1
-rw-r--r--apps/dav/composer/composer/autoload_static.php1
-rw-r--r--apps/dav/l10n/ar.js4
-rw-r--r--apps/dav/l10n/ar.json4
-rw-r--r--apps/dav/l10n/es.js16
-rw-r--r--apps/dav/l10n/es.json16
-rw-r--r--apps/dav/l10n/ja.js2
-rw-r--r--apps/dav/l10n/ja.json2
-rw-r--r--apps/dav/lib/CalDAV/Activity/Provider/Base.php6
-rw-r--r--apps/dav/lib/CalDAV/Schedule/IMipPlugin.php12
-rw-r--r--apps/dav/lib/CalDAV/Schedule/Plugin.php10
-rw-r--r--apps/dav/lib/CalDAV/TipBroker.php187
-rw-r--r--apps/dav/lib/Files/Sharing/FilesDropPlugin.php2
-rw-r--r--apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php19
-rw-r--r--apps/dav/tests/unit/CalDAV/TipBrokerTest.php178
15 files changed, 447 insertions, 13 deletions
diff --git a/apps/dav/composer/composer/autoload_classmap.php b/apps/dav/composer/composer/autoload_classmap.php
index 43c3cb12ddb..446625779a3 100644
--- a/apps/dav/composer/composer/autoload_classmap.php
+++ b/apps/dav/composer/composer/autoload_classmap.php
@@ -111,6 +111,7 @@ return array(
'OCA\\DAV\\CalDAV\\Sharing\\Service' => $baseDir . '/../lib/CalDAV/Sharing/Service.php',
'OCA\\DAV\\CalDAV\\Status\\StatusService' => $baseDir . '/../lib/CalDAV/Status/StatusService.php',
'OCA\\DAV\\CalDAV\\TimezoneService' => $baseDir . '/../lib/CalDAV/TimezoneService.php',
+ 'OCA\\DAV\\CalDAV\\TipBroker' => $baseDir . '/../lib/CalDAV/TipBroker.php',
'OCA\\DAV\\CalDAV\\Trashbin\\DeletedCalendarObject' => $baseDir . '/../lib/CalDAV/Trashbin/DeletedCalendarObject.php',
'OCA\\DAV\\CalDAV\\Trashbin\\DeletedCalendarObjectsCollection' => $baseDir . '/../lib/CalDAV/Trashbin/DeletedCalendarObjectsCollection.php',
'OCA\\DAV\\CalDAV\\Trashbin\\Plugin' => $baseDir . '/../lib/CalDAV/Trashbin/Plugin.php',
diff --git a/apps/dav/composer/composer/autoload_static.php b/apps/dav/composer/composer/autoload_static.php
index fd49b5fb45c..4457be9bd35 100644
--- a/apps/dav/composer/composer/autoload_static.php
+++ b/apps/dav/composer/composer/autoload_static.php
@@ -126,6 +126,7 @@ class ComposerStaticInitDAV
'OCA\\DAV\\CalDAV\\Sharing\\Service' => __DIR__ . '/..' . '/../lib/CalDAV/Sharing/Service.php',
'OCA\\DAV\\CalDAV\\Status\\StatusService' => __DIR__ . '/..' . '/../lib/CalDAV/Status/StatusService.php',
'OCA\\DAV\\CalDAV\\TimezoneService' => __DIR__ . '/..' . '/../lib/CalDAV/TimezoneService.php',
+ 'OCA\\DAV\\CalDAV\\TipBroker' => __DIR__ . '/..' . '/../lib/CalDAV/TipBroker.php',
'OCA\\DAV\\CalDAV\\Trashbin\\DeletedCalendarObject' => __DIR__ . '/..' . '/../lib/CalDAV/Trashbin/DeletedCalendarObject.php',
'OCA\\DAV\\CalDAV\\Trashbin\\DeletedCalendarObjectsCollection' => __DIR__ . '/..' . '/../lib/CalDAV/Trashbin/DeletedCalendarObjectsCollection.php',
'OCA\\DAV\\CalDAV\\Trashbin\\Plugin' => __DIR__ . '/..' . '/../lib/CalDAV/Trashbin/Plugin.php',
diff --git a/apps/dav/l10n/ar.js b/apps/dav/l10n/ar.js
index 79a6bde6cea..44d19c85a6f 100644
--- a/apps/dav/l10n/ar.js
+++ b/apps/dav/l10n/ar.js
@@ -251,9 +251,9 @@ OC.L10N.register(
"Failed to clear your absence settings" : "تعذّر محو إعداداتك للغياب",
"Time zone:" : "منطقة زمنية:",
"to" : "إلى",
- "Delete slot" : "حذف فُرَضَةٍ slot زمنيّةٍ",
+ "Delete slot" : "حذف الخانة الزمنية",
"No working hours set" : "لم يتم تحديد ساعات العمل",
- "Add slot" : "إضافة فترة زمنية",
+ "Add slot" : "إضافة خانة زمنية",
"Weekdays" : "أيام الأسبوع",
"Pick a start time for {dayName}" : "إختَر وقت البدء ليوم {dayName}",
"Pick a end time for {dayName}" : "إختَر وقت الانتهاء ليوم {dayName}",
diff --git a/apps/dav/l10n/ar.json b/apps/dav/l10n/ar.json
index 3b8e353b73f..3344d868bbb 100644
--- a/apps/dav/l10n/ar.json
+++ b/apps/dav/l10n/ar.json
@@ -249,9 +249,9 @@
"Failed to clear your absence settings" : "تعذّر محو إعداداتك للغياب",
"Time zone:" : "منطقة زمنية:",
"to" : "إلى",
- "Delete slot" : "حذف فُرَضَةٍ slot زمنيّةٍ",
+ "Delete slot" : "حذف الخانة الزمنية",
"No working hours set" : "لم يتم تحديد ساعات العمل",
- "Add slot" : "إضافة فترة زمنية",
+ "Add slot" : "إضافة خانة زمنية",
"Weekdays" : "أيام الأسبوع",
"Pick a start time for {dayName}" : "إختَر وقت البدء ليوم {dayName}",
"Pick a end time for {dayName}" : "إختَر وقت الانتهاء ليوم {dayName}",
diff --git a/apps/dav/l10n/es.js b/apps/dav/l10n/es.js
index 5092422edf8..50e8fadc6f1 100644
--- a/apps/dav/l10n/es.js
+++ b/apps/dav/l10n/es.js
@@ -72,9 +72,25 @@ OC.L10N.register(
"Description: %s" : "Descripción: %s",
"Where: %s" : "Lugar: %s",
"%1$s via %2$s" : "%1$s vía %2$s",
+ "In a %1$s on %2$s for the entire day" : "En un(a) %1$s el %2$s todo el día",
+ "In a %1$s on %2$s between %3$s - %4$s" : "En un(a) %1$s el %2$s entre las %3$s - %4$s",
+ "In %1$s %2$s on %3$s for the entire day" : "En %1$s %2$s el %3$s todo el día",
+ "In %1$s %2$s on %3$s between %4$s - %5$s" : "En %1$s %2$s el %3$s entre las %4$s - %5$s",
+ "Could not generate when statement" : "No se ha podido general la declaración de cuándo",
"Every Day for the entire day" : "Todos los días durante todo el día",
+ "Every Day for the entire day until %1$s" : "Cada day todo el día hasta el %1$s",
"Every Day between %1$s - %2$s" : "Todos los días entre %1$s - %2$s",
"Every Day between %1$s - %2$s until %3$s" : "Todos los días entre %1$s - %2$s hasta %3$s",
+ "Every %1$d Days for the entire day" : "Cada %1$d días todo el día",
+ "Every %1$d Days for the entire day until %2$s" : "Cada %1$d días todo el día hasta el %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "Cada %1$d días entre las %2$s y %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "Cada %1$d días entre las %2$s y %3$s hasta el %4$s",
+ "Could not generate event recurrence statement" : "No se ha podido generar la declaración de recurrencia",
+ "Every Week on %1$s for the entire day" : "Cada semana el %1$s todo el día",
+ "Every Week on %1$s for the entire day until %2$s" : "Cada %1$s días todo el día hasta el %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "Cada semana el %1$s entre %2$s y %3$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Cada semana el %1$s entre %2$s y %3$s hasta el %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "Cada %1$d semanas el %2$s todo el día",
"Cancelled: %1$s" : "Cancelado: %1$s",
"\"%1$s\" has been canceled" : "\"%1$s\" ha sido cancelada",
"Re: %1$s" : "Re: %1$s",
diff --git a/apps/dav/l10n/es.json b/apps/dav/l10n/es.json
index cec872c26ac..44ab0e3ee55 100644
--- a/apps/dav/l10n/es.json
+++ b/apps/dav/l10n/es.json
@@ -70,9 +70,25 @@
"Description: %s" : "Descripción: %s",
"Where: %s" : "Lugar: %s",
"%1$s via %2$s" : "%1$s vía %2$s",
+ "In a %1$s on %2$s for the entire day" : "En un(a) %1$s el %2$s todo el día",
+ "In a %1$s on %2$s between %3$s - %4$s" : "En un(a) %1$s el %2$s entre las %3$s - %4$s",
+ "In %1$s %2$s on %3$s for the entire day" : "En %1$s %2$s el %3$s todo el día",
+ "In %1$s %2$s on %3$s between %4$s - %5$s" : "En %1$s %2$s el %3$s entre las %4$s - %5$s",
+ "Could not generate when statement" : "No se ha podido general la declaración de cuándo",
"Every Day for the entire day" : "Todos los días durante todo el día",
+ "Every Day for the entire day until %1$s" : "Cada day todo el día hasta el %1$s",
"Every Day between %1$s - %2$s" : "Todos los días entre %1$s - %2$s",
"Every Day between %1$s - %2$s until %3$s" : "Todos los días entre %1$s - %2$s hasta %3$s",
+ "Every %1$d Days for the entire day" : "Cada %1$d días todo el día",
+ "Every %1$d Days for the entire day until %2$s" : "Cada %1$d días todo el día hasta el %2$s",
+ "Every %1$d Days between %2$s - %3$s" : "Cada %1$d días entre las %2$s y %3$s",
+ "Every %1$d Days between %2$s - %3$s until %4$s" : "Cada %1$d días entre las %2$s y %3$s hasta el %4$s",
+ "Could not generate event recurrence statement" : "No se ha podido generar la declaración de recurrencia",
+ "Every Week on %1$s for the entire day" : "Cada semana el %1$s todo el día",
+ "Every Week on %1$s for the entire day until %2$s" : "Cada %1$s días todo el día hasta el %2$s",
+ "Every Week on %1$s between %2$s - %3$s" : "Cada semana el %1$s entre %2$s y %3$s",
+ "Every Week on %1$s between %2$s - %3$s until %4$s" : "Cada semana el %1$s entre %2$s y %3$s hasta el %4$s",
+ "Every %1$d Weeks on %2$s for the entire day" : "Cada %1$d semanas el %2$s todo el día",
"Cancelled: %1$s" : "Cancelado: %1$s",
"\"%1$s\" has been canceled" : "\"%1$s\" ha sido cancelada",
"Re: %1$s" : "Re: %1$s",
diff --git a/apps/dav/l10n/ja.js b/apps/dav/l10n/ja.js
index 430aadf6069..9b927c6874e 100644
--- a/apps/dav/l10n/ja.js
+++ b/apps/dav/l10n/ja.js
@@ -64,7 +64,7 @@ OC.L10N.register(
"_%n month_::_%n months_" : ["%nヶ月"],
"_%n day_::_%n days_" : ["%n日"],
"_%n hour_::_%n hours_" : ["%n時間"],
- "_%n minute_::_%n minutes_" : ["%n秒"],
+ "_%n minute_::_%n minutes_" : ["%n分钟"],
"%s (in %s)" : "%s(%s後)",
"%s (%s ago)" : "%s(%s前)",
"Calendar: %s" : "カレンダー:%s",
diff --git a/apps/dav/l10n/ja.json b/apps/dav/l10n/ja.json
index 7dc9f60f4e2..a1316c2fd05 100644
--- a/apps/dav/l10n/ja.json
+++ b/apps/dav/l10n/ja.json
@@ -62,7 +62,7 @@
"_%n month_::_%n months_" : ["%nヶ月"],
"_%n day_::_%n days_" : ["%n日"],
"_%n hour_::_%n hours_" : ["%n時間"],
- "_%n minute_::_%n minutes_" : ["%n秒"],
+ "_%n minute_::_%n minutes_" : ["%n分钟"],
"%s (in %s)" : "%s(%s後)",
"%s (%s ago)" : "%s(%s前)",
"Calendar: %s" : "カレンダー:%s",
diff --git a/apps/dav/lib/CalDAV/Activity/Provider/Base.php b/apps/dav/lib/CalDAV/Activity/Provider/Base.php
index a063a31d015..14e68cc6a3d 100644
--- a/apps/dav/lib/CalDAV/Activity/Provider/Base.php
+++ b/apps/dav/lib/CalDAV/Activity/Provider/Base.php
@@ -52,14 +52,14 @@ abstract class Base implements IProvider {
$data['name'] === CalDavBackend::PERSONAL_CALENDAR_NAME) {
return [
'type' => 'calendar',
- 'id' => $data['id'],
+ 'id' => (string)$data['id'],
'name' => $l->t('Personal'),
];
}
return [
'type' => 'calendar',
- 'id' => $data['id'],
+ 'id' => (string)$data['id'],
'name' => $data['name'],
];
}
@@ -72,7 +72,7 @@ abstract class Base implements IProvider {
protected function generateLegacyCalendarParameter($id, $name) {
return [
'type' => 'calendar',
- 'id' => $id,
+ 'id' => (string)$id,
'name' => $name,
];
}
diff --git a/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php b/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php
index 1958531630a..1cfe8f47cb5 100644
--- a/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php
+++ b/apps/dav/lib/CalDAV/Schedule/IMipPlugin.php
@@ -109,8 +109,16 @@ class IMipPlugin extends SabreIMipPlugin {
* @return void
*/
public function schedule(Message $iTipMessage) {
- // Not sending any emails if the system considers the update
- // insignificant.
+
+ // do not send imip messages if external system already did
+ /** @psalm-suppress UndefinedPropertyFetch */
+ if ($iTipMessage->message?->VEVENT?->{'X-NC-DISABLE-SCHEDULING'}?->getValue() === 'true') {
+ if (!$iTipMessage->scheduleStatus) {
+ $iTipMessage->scheduleStatus = '1.0;We got the message, but iMip messages are disabled for this event';
+ }
+ return;
+ }
+ // Not sending any emails if the system considers the update insignificant
if (!$iTipMessage->significantChange) {
if (!$iTipMessage->scheduleStatus) {
$iTipMessage->scheduleStatus = '1.0;We got the message, but it\'s not significant enough to warrant an email';
diff --git a/apps/dav/lib/CalDAV/Schedule/Plugin.php b/apps/dav/lib/CalDAV/Schedule/Plugin.php
index 48f4cbf22ef..8e5ba282f72 100644
--- a/apps/dav/lib/CalDAV/Schedule/Plugin.php
+++ b/apps/dav/lib/CalDAV/Schedule/Plugin.php
@@ -10,6 +10,7 @@ use OCA\DAV\CalDAV\CalDavBackend;
use OCA\DAV\CalDAV\Calendar;
use OCA\DAV\CalDAV\CalendarHome;
use OCA\DAV\CalDAV\DefaultCalendarValidator;
+use OCA\DAV\CalDAV\TipBroker;
use OCP\IConfig;
use Psr\Log\LoggerInterface;
use Sabre\CalDAV\ICalendar;
@@ -85,6 +86,13 @@ class Plugin extends \Sabre\CalDAV\Schedule\Plugin {
}
/**
+ * Returns an instance of the iTip\Broker.
+ */
+ protected function createITipBroker(): TipBroker {
+ return new TipBroker();
+ }
+
+ /**
* Allow manual setting of the object change URL
* to support public write
*
@@ -167,7 +175,7 @@ class Plugin extends \Sabre\CalDAV\Schedule\Plugin {
$calendarNode = $this->server->tree->getNodeForPath($calendarPath);
// extract addresses for owner
$addresses = $this->getAddressesForPrincipal($calendarNode->getOwner());
- // determain if request is from a sharee
+ // determine if request is from a sharee
if ($calendarNode->isShared()) {
// extract addresses for sharee and add to address collection
$addresses = array_merge(
diff --git a/apps/dav/lib/CalDAV/TipBroker.php b/apps/dav/lib/CalDAV/TipBroker.php
new file mode 100644
index 00000000000..43eff124f0b
--- /dev/null
+++ b/apps/dav/lib/CalDAV/TipBroker.php
@@ -0,0 +1,187 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\DAV\CalDAV;
+
+use Sabre\VObject\Component\VCalendar;
+use Sabre\VObject\ITip\Broker;
+use Sabre\VObject\ITip\Message;
+
+class TipBroker extends Broker {
+
+ public $significantChangeProperties = [
+ 'DTSTART',
+ 'DTEND',
+ 'DURATION',
+ 'DUE',
+ 'RRULE',
+ 'RDATE',
+ 'EXDATE',
+ 'STATUS',
+ 'SUMMARY',
+ 'DESCRIPTION',
+ 'LOCATION',
+
+ ];
+
+ /**
+ * This method is used in cases where an event got updated, and we
+ * potentially need to send emails to attendees to let them know of updates
+ * in the events.
+ *
+ * We will detect which attendees got added, which got removed and create
+ * specific messages for these situations.
+ *
+ * @return array
+ */
+ protected function parseEventForOrganizer(VCalendar $calendar, array $eventInfo, array $oldEventInfo) {
+ // Merging attendee lists.
+ $attendees = [];
+ foreach ($oldEventInfo['attendees'] as $attendee) {
+ $attendees[$attendee['href']] = [
+ 'href' => $attendee['href'],
+ 'oldInstances' => $attendee['instances'],
+ 'newInstances' => [],
+ 'name' => $attendee['name'],
+ 'forceSend' => null,
+ ];
+ }
+ foreach ($eventInfo['attendees'] as $attendee) {
+ if (isset($attendees[$attendee['href']])) {
+ $attendees[$attendee['href']]['name'] = $attendee['name'];
+ $attendees[$attendee['href']]['newInstances'] = $attendee['instances'];
+ $attendees[$attendee['href']]['forceSend'] = $attendee['forceSend'];
+ } else {
+ $attendees[$attendee['href']] = [
+ 'href' => $attendee['href'],
+ 'oldInstances' => [],
+ 'newInstances' => $attendee['instances'],
+ 'name' => $attendee['name'],
+ 'forceSend' => $attendee['forceSend'],
+ ];
+ }
+ }
+
+ $messages = [];
+
+ foreach ($attendees as $attendee) {
+ // An organizer can also be an attendee. We should not generate any
+ // messages for those.
+ if ($attendee['href'] === $eventInfo['organizer']) {
+ continue;
+ }
+
+ $message = new Message();
+ $message->uid = $eventInfo['uid'];
+ $message->component = 'VEVENT';
+ $message->sequence = $eventInfo['sequence'];
+ $message->sender = $eventInfo['organizer'];
+ $message->senderName = $eventInfo['organizerName'];
+ $message->recipient = $attendee['href'];
+ $message->recipientName = $attendee['name'];
+
+ // Creating the new iCalendar body.
+ $icalMsg = new VCalendar();
+
+ foreach ($calendar->select('VTIMEZONE') as $timezone) {
+ $icalMsg->add(clone $timezone);
+ }
+ // If there are no instances the attendee is a part of, it means
+ // the attendee was removed and we need to send them a CANCEL message.
+ // Also If the meeting STATUS property was changed to CANCELLED
+ // we need to send the attendee a CANCEL message.
+ if (!$attendee['newInstances'] || $eventInfo['status'] === 'CANCELLED') {
+
+ $message->method = $icalMsg->METHOD = 'CANCEL';
+ $message->significantChange = true;
+ // clone base event
+ $event = clone $eventInfo['instances']['master'];
+ // alter some properties
+ unset($event->ATTENDEE);
+ $event->add('ATTENDEE', $attendee['href'], ['CN' => $attendee['name'],]);
+ $event->DTSTAMP = gmdate('Ymd\\THis\\Z');
+ $event->SEQUENCE = $message->sequence;
+ $icalMsg->add($event);
+
+ } else {
+ // The attendee gets the updated event body
+ $message->method = $icalMsg->METHOD = 'REQUEST';
+
+ // We need to find out that this change is significant. If it's
+ // not, systems may opt to not send messages.
+ //
+ // We do this based on the 'significantChangeHash' which is
+ // some value that changes if there's a certain set of
+ // properties changed in the event, or simply if there's a
+ // difference in instances that the attendee is invited to.
+
+ $oldAttendeeInstances = array_keys($attendee['oldInstances']);
+ $newAttendeeInstances = array_keys($attendee['newInstances']);
+
+ $message->significantChange =
+ $attendee['forceSend'] === 'REQUEST' ||
+ count($oldAttendeeInstances) !== count($newAttendeeInstances) ||
+ count(array_diff($oldAttendeeInstances, $newAttendeeInstances)) > 0 ||
+ $oldEventInfo['significantChangeHash'] !== $eventInfo['significantChangeHash'];
+
+ foreach ($attendee['newInstances'] as $instanceId => $instanceInfo) {
+ $currentEvent = clone $eventInfo['instances'][$instanceId];
+ if ($instanceId === 'master') {
+ // We need to find a list of events that the attendee
+ // is not a part of to add to the list of exceptions.
+ $exceptions = [];
+ foreach ($eventInfo['instances'] as $instanceId => $vevent) {
+ if (!isset($attendee['newInstances'][$instanceId])) {
+ $exceptions[] = $instanceId;
+ }
+ }
+
+ // If there were exceptions, we need to add it to an
+ // existing EXDATE property, if it exists.
+ if ($exceptions) {
+ if (isset($currentEvent->EXDATE)) {
+ $currentEvent->EXDATE->setParts(array_merge(
+ $currentEvent->EXDATE->getParts(),
+ $exceptions
+ ));
+ } else {
+ $currentEvent->EXDATE = $exceptions;
+ }
+ }
+
+ // Cleaning up any scheduling information that
+ // shouldn't be sent along.
+ unset($currentEvent->ORGANIZER['SCHEDULE-FORCE-SEND']);
+ unset($currentEvent->ORGANIZER['SCHEDULE-STATUS']);
+
+ foreach ($currentEvent->ATTENDEE as $attendee) {
+ unset($attendee['SCHEDULE-FORCE-SEND']);
+ unset($attendee['SCHEDULE-STATUS']);
+
+ // We're adding PARTSTAT=NEEDS-ACTION to ensure that
+ // iOS shows an "Inbox Item"
+ if (!isset($attendee['PARTSTAT'])) {
+ $attendee['PARTSTAT'] = 'NEEDS-ACTION';
+ }
+ }
+ }
+
+ $currentEvent->DTSTAMP = gmdate('Ymd\\THis\\Z');
+ $icalMsg->add($currentEvent);
+ }
+ }
+
+ $message->message = $icalMsg;
+ $messages[] = $message;
+ }
+
+ return $messages;
+ }
+
+}
diff --git a/apps/dav/lib/Files/Sharing/FilesDropPlugin.php b/apps/dav/lib/Files/Sharing/FilesDropPlugin.php
index 69328d42272..9d883be81fc 100644
--- a/apps/dav/lib/Files/Sharing/FilesDropPlugin.php
+++ b/apps/dav/lib/Files/Sharing/FilesDropPlugin.php
@@ -64,7 +64,7 @@ class FilesDropPlugin extends ServerPlugin {
// Extract the attributes for the file request
$isFileRequest = false;
$attributes = $this->share->getAttributes();
- $nickName = $request->getHeader('X-NC-Nickname');
+ $nickName = $request->hasHeader('X-NC-Nickname') ? urldecode($request->getHeader('X-NC-Nickname')) : null;
if ($attributes !== null) {
$isFileRequest = $attributes->getAttribute('fileRequest', 'enabled') === true;
}
diff --git a/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php b/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php
index bda3182d117..c8dfe257bf2 100644
--- a/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php
+++ b/apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php
@@ -890,4 +890,23 @@ class IMipPluginTest extends TestCase {
$this->plugin->schedule($message);
$this->assertEquals('1.1', $message->getScheduleStatus());
}
+
+ public function testImipDisabledForEvent(): void {
+ // construct iTip message with event and attendees
+ $calendar = new VCalendar();
+ $calendar->add('VEVENT', ['UID' => 'uid-1234']);
+ $event = $calendar->VEVENT;
+ $event->add('ORGANIZER', 'mailto:gandalf@wiz.ard');
+ $event->add('ATTENDEE', 'mailto:' . 'frodo@hobb.it', ['RSVP' => 'TRUE', 'CN' => 'Frodo']);
+ $event->add('X-NC-DISABLE-SCHEDULING', 'true');
+ $message = new Message();
+ $message->method = 'REQUEST';
+ $message->message = $calendar;
+ $message->sender = 'mailto:gandalf@wiz.ard';
+ $message->senderName = 'Mr. Wizard';
+ $message->recipient = 'mailto:' . 'frodo@hobb.it';
+
+ $this->plugin->schedule($message);
+ $this->assertEquals('1.0;We got the message, but iMip messages are disabled for this event', $message->scheduleStatus);
+ }
}
diff --git a/apps/dav/tests/unit/CalDAV/TipBrokerTest.php b/apps/dav/tests/unit/CalDAV/TipBrokerTest.php
new file mode 100644
index 00000000000..3a8e240c1d8
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/TipBrokerTest.php
@@ -0,0 +1,178 @@
+<?php
+/**
+ * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\DAV\Tests\unit\CalDAV;
+
+use OCA\DAV\CalDAV\TipBroker;
+use Sabre\VObject\Component\VCalendar;
+use Test\TestCase;
+
+class TipBrokerTest extends TestCase {
+
+ private TipBroker $broker;
+ private VCalendar $vCalendar1a;
+
+ protected function setUp(): void {
+ parent::setUp();
+
+ $this->broker = new TipBroker();
+ // construct calendar with a 1 hour event and same start/end time zones
+ $this->vCalendar1a = new VCalendar();
+ /** @var VEvent $vEvent */
+ $vEvent = $this->vCalendar1a->add('VEVENT', []);
+ $vEvent->add('UID', '96a0e6b1-d886-4a55-a60d-152b31401dcc');
+ $vEvent->add('DTSTAMP', '20240701T000000Z');
+ $vEvent->add('CREATED', '20240701T000000Z');
+ $vEvent->add('LAST-MODIFIED', '20240701T000000Z');
+ $vEvent->add('SEQUENCE', '1');
+ $vEvent->add('STATUS', 'CONFIRMED');
+ $vEvent->add('DTSTART', '20240701T080000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('DTEND', '20240701T090000', ['TZID' => 'America/Toronto']);
+ $vEvent->add('SUMMARY', 'Test Event');
+ $vEvent->add('ORGANIZER', 'mailto:organizer@testing.com', ['CN' => 'Organizer']);
+ $vEvent->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+ }
+
+ public function testParseEventForOrganizerOnCreate(): void {
+
+ // construct calendar and generate event info for newly created event with one attendee
+ $calendar = clone $this->vCalendar1a;
+ $previousEventInfo = [
+ 'organizer' => null,
+ 'significantChangeHash' => '',
+ 'attendees' => [],
+ ];
+ $currentEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]);
+ // test iTip generation
+ $messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$calendar, $currentEventInfo, $previousEventInfo]);
+ $this->assertCount(1, $messages);
+ $this->assertEquals('REQUEST', $messages[0]->method);
+ $this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender);
+ $this->assertEquals($calendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient);
+
+ }
+
+ public function testParseEventForOrganizerOnModify(): void {
+
+ // construct calendar and generate event info for modified event with one attendee
+ $calendar = clone $this->vCalendar1a;
+ $previousEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]);
+ $calendar->VEVENT->{'LAST-MODIFIED'}->setValue('20240701T020000Z');
+ $calendar->VEVENT->SEQUENCE->setValue(2);
+ $calendar->VEVENT->SUMMARY->setValue('Test Event Modified');
+ $currentEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]);
+ // test iTip generation
+ $messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$calendar, $currentEventInfo, $previousEventInfo]);
+ $this->assertCount(1, $messages);
+ $this->assertEquals('REQUEST', $messages[0]->method);
+ $this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender);
+ $this->assertEquals($calendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient);
+
+ }
+
+ public function testParseEventForOrganizerOnDelete(): void {
+
+ // construct calendar and generate event info for modified event with one attendee
+ $calendar = clone $this->vCalendar1a;
+ $previousEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]);
+ $currentEventInfo = $previousEventInfo;
+ $currentEventInfo['attendees'] = [];
+ ++$currentEventInfo['sequence'];
+ // test iTip generation
+ $messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$calendar, $currentEventInfo, $previousEventInfo]);
+ $this->assertCount(1, $messages);
+ $this->assertEquals('CANCEL', $messages[0]->method);
+ $this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender);
+ $this->assertEquals($calendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient);
+
+ }
+
+ public function testParseEventForOrganizerOnStatusCancelled(): void {
+
+ // construct calendar and generate event info for modified event with one attendee
+ $calendar = clone $this->vCalendar1a;
+ $previousEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]);
+ $calendar->VEVENT->{'LAST-MODIFIED'}->setValue('20240701T020000Z');
+ $calendar->VEVENT->SEQUENCE->setValue(2);
+ $calendar->VEVENT->STATUS->setValue('CANCELLED');
+ $currentEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]);
+ // test iTip generation
+ $messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$calendar, $currentEventInfo, $previousEventInfo]);
+ $this->assertCount(1, $messages);
+ $this->assertEquals('CANCEL', $messages[0]->method);
+ $this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender);
+ $this->assertEquals($calendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient);
+
+ }
+
+ public function testParseEventForOrganizerOnAddAttendee(): void {
+
+ // construct calendar and generate event info for modified event with two attendees
+ $calendar = clone $this->vCalendar1a;
+ $previousEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]);
+ $calendar->VEVENT->{'LAST-MODIFIED'}->setValue('20240701T020000Z');
+ $calendar->VEVENT->SEQUENCE->setValue(2);
+ $calendar->VEVENT->add('ATTENDEE', 'mailto:attendee2@testing.com', [
+ 'CN' => 'Attendee Two',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+ $currentEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]);
+ // test iTip generation
+ $messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$calendar, $currentEventInfo, $previousEventInfo]);
+ $this->assertCount(2, $messages);
+ $this->assertEquals('REQUEST', $messages[0]->method);
+ $this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender);
+ $this->assertEquals($calendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient);
+ $this->assertEquals('REQUEST', $messages[1]->method);
+ $this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[1]->sender);
+ $this->assertEquals($calendar->VEVENT->ATTENDEE[1]->getValue(), $messages[1]->recipient);
+
+ }
+
+ public function testParseEventForOrganizerOnRemoveAttendee(): void {
+
+ // construct calendar and generate event info for modified event with two attendees
+ $calendar = clone $this->vCalendar1a;
+ $calendar->VEVENT->add('ATTENDEE', 'mailto:attendee2@testing.com', [
+ 'CN' => 'Attendee Two',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+ $previousEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]);
+ $calendar->VEVENT->{'LAST-MODIFIED'}->setValue('20240701T020000Z');
+ $calendar->VEVENT->SEQUENCE->setValue(2);
+ $calendar->VEVENT->remove('ATTENDEE');
+ $calendar->VEVENT->add('ATTENDEE', 'mailto:attendee1@testing.com', [
+ 'CN' => 'Attendee One',
+ 'CUTYPE' => 'INDIVIDUAL',
+ 'PARTSTAT' => 'NEEDS-ACTION',
+ 'ROLE' => 'REQ-PARTICIPANT',
+ 'RSVP' => 'TRUE'
+ ]);
+ $currentEventInfo = $this->invokePrivate($this->broker, 'parseEventInfo', [$calendar]);
+ // test iTip generation
+ $messages = $this->invokePrivate($this->broker, 'parseEventForOrganizer', [$calendar, $currentEventInfo, $previousEventInfo]);
+ $this->assertCount(2, $messages);
+ $this->assertEquals('REQUEST', $messages[0]->method);
+ $this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[0]->sender);
+ $this->assertEquals($calendar->VEVENT->ATTENDEE[0]->getValue(), $messages[0]->recipient);
+ $this->assertEquals('CANCEL', $messages[1]->method);
+ $this->assertEquals($calendar->VEVENT->ORGANIZER->getValue(), $messages[1]->sender);
+ $this->assertEquals('mailto:attendee2@testing.com', $messages[1]->recipient);
+
+ }
+
+}