diff options
Diffstat (limited to 'apps/dav')
-rw-r--r-- | apps/dav/composer/composer/autoload_classmap.php | 1 | ||||
-rw-r--r-- | apps/dav/composer/composer/autoload_static.php | 1 | ||||
-rw-r--r-- | apps/dav/l10n/ar.js | 4 | ||||
-rw-r--r-- | apps/dav/l10n/ar.json | 4 | ||||
-rw-r--r-- | apps/dav/l10n/es.js | 16 | ||||
-rw-r--r-- | apps/dav/l10n/es.json | 16 | ||||
-rw-r--r-- | apps/dav/l10n/ja.js | 2 | ||||
-rw-r--r-- | apps/dav/l10n/ja.json | 2 | ||||
-rw-r--r-- | apps/dav/lib/CalDAV/Activity/Provider/Base.php | 6 | ||||
-rw-r--r-- | apps/dav/lib/CalDAV/Schedule/IMipPlugin.php | 12 | ||||
-rw-r--r-- | apps/dav/lib/CalDAV/Schedule/Plugin.php | 10 | ||||
-rw-r--r-- | apps/dav/lib/CalDAV/TipBroker.php | 187 | ||||
-rw-r--r-- | apps/dav/lib/Files/Sharing/FilesDropPlugin.php | 2 | ||||
-rw-r--r-- | apps/dav/tests/unit/CalDAV/Schedule/IMipPluginTest.php | 19 | ||||
-rw-r--r-- | apps/dav/tests/unit/CalDAV/TipBrokerTest.php | 178 |
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); + + } + +} |