From 97cea7e90bf05e679a478c3d11e99830ceb9e436 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Stefan=20Tollk=C3=BChn?= Date: Thu, 22 Oct 2026 19:23:25 +0200 Subject: [PATCH] Make calendar owner the event organizer The ICS organizer was taken from the external link's organizer parameter. The CalDAV backend only schedules invitations when the organizer matches the calendar owner, so invites never went out under that value. The event now names the authenticated user's email address; the link organizer remains the fallback for users without a profile email and stays visible on the confirm page as meeting information. --- lib/Controller/CalendarController.php | 18 ++++- tests/Unit/CalendarControllerTest.php | 105 ++++++++++++++++++++++++++ 2 files changed, 121 insertions(+), 2 deletions(-) diff --git a/lib/Controller/CalendarController.php b/lib/Controller/CalendarController.php index 884d585..d4ad5f9 100644 --- a/lib/Controller/CalendarController.php +++ b/lib/Controller/CalendarController.php @@ -198,6 +198,12 @@ class CalendarController extends Controller throw new \RuntimeException('No authenticated Nextcloud user.'); } + /* + * The calendar owner must be the ICS organizer so the + * CalDAV backend schedules the invitations in their name. + */ + $data['organizerEmail'] = $user->getEMailAddress() ?? ''; + $calendars = $this->findWritableCalendars($user->getUID()); /* @@ -582,8 +588,16 @@ class CalendarController extends Controller $builder->setLocation($data['location']); } - if ($data['organizer'] !== '') { - $builder->setOrganizer($data['organizer']); + /* + * Invitations come from the calendar owner, the + * organizer of the external link is the fallback. + */ + $organizer = $data['organizerEmail'] !== '' + ? $data['organizerEmail'] + : $data['organizer']; + + if ($organizer !== '') { + $builder->setOrganizer($organizer); } /* diff --git a/tests/Unit/CalendarControllerTest.php b/tests/Unit/CalendarControllerTest.php index 11cf848..ee84675 100644 --- a/tests/Unit/CalendarControllerTest.php +++ b/tests/Unit/CalendarControllerTest.php @@ -489,6 +489,7 @@ class CalendarControllerTest extends TestCase 'start' => new \DateTimeImmutable('2026-10-07T14:15:00 UTC'), 'end' => new \DateTimeImmutable('2026-10-07T15:15:00 UTC'), 'organizer' => '', + 'organizerEmail' => '', 'location' => '', 'url' => '', 'attendees' => [], @@ -539,6 +540,7 @@ class CalendarControllerTest extends TestCase 'start' => new \DateTimeImmutable('2026-10-07T14:15:00 UTC'), 'end' => new \DateTimeImmutable('2026-10-07T15:15:00 UTC'), 'organizer' => '', + 'organizerEmail' => '', 'location' => '', 'url' => '', 'attendees' => [ @@ -550,6 +552,109 @@ class CalendarControllerTest extends TestCase $this->invoke($c, 'createEvent', [$calendar, $data]); } + /* + * createEvent: the calendar owner is the ICS organizer + */ + + private function skeletonIcs(): string + { + return + "BEGIN:VCALENDAR\r\n" . + "VERSION:2.0\r\n" . + "PRODID:-//Nextcloud//AToCal//EN\r\n" . + "BEGIN:VEVENT\r\n" . + "UID:test-uid-123\r\n" . + "SUMMARY:Weekly sync\r\n" . + "END:VEVENT\r\n" . + "END:VCALENDAR\r\n"; + } + + private function eventBuilderWith(\PHPUnit\Framework\MockObject\MockObject|ICalendarEventBuilder $builder): void + { + $builder->method('toIcs')->willReturn($this->skeletonIcs()); + $this->calendarManager->method('createEventBuilder')->willReturn($builder); + } + + public function testCreateEventPrefersUserEmailAsOrganizer(): void + { + $builder = $this->createMock(ICalendarEventBuilder::class); + $builder->expects($this->once())->method('setOrganizer') + ->with('alice@nextcloud.example'); + $this->eventBuilderWith($builder); + + $calendar = $this->mockCalendar('5', 'personal', 'Personal', Constants::PERMISSION_CREATE); + + $c = $this->newController(); + + $data = [ + 'summary' => 'Weekly sync', + 'description' => '', + 'start' => new \DateTimeImmutable('2026-10-07T14:15:00 UTC'), + 'end' => new \DateTimeImmutable('2026-10-07T15:15:00 UTC'), + 'organizer' => 'boss@example.com', + 'organizerEmail' => 'alice@nextcloud.example', + 'location' => '', + 'url' => '', + 'attendees' => [], + ]; + + $this->invoke($c, 'createEvent', [$calendar, $data]); + } + + public function testCreateEventFallsBackToLinkOrganizer(): void + { + $builder = $this->createMock(ICalendarEventBuilder::class); + $builder->expects($this->once())->method('setOrganizer') + ->with('boss@example.com'); + $this->eventBuilderWith($builder); + + $calendar = $this->mockCalendar('5', 'personal', 'Personal', Constants::PERMISSION_CREATE); + + $c = $this->newController(); + + $data = [ + 'summary' => 'Weekly sync', + 'description' => '', + 'start' => new \DateTimeImmutable('2026-10-07T14:15:00 UTC'), + 'end' => new \DateTimeImmutable('2026-10-07T15:15:00 UTC'), + 'organizer' => 'boss@example.com', + 'organizerEmail' => '', + 'location' => '', + 'url' => '', + 'attendees' => [], + ]; + + $this->invoke($c, 'createEvent', [$calendar, $data]); + } + + /* + * create(): the user's email address reaches the event builder + */ + + public function testCreateUsesUserEmailAsOrganizer(): void + { + $user = $this->mockUser('alice'); + $user->method('getEMailAddress')->willReturn('alice@nextcloud.example'); + $this->userSession->method('getUser')->willReturn($user); + $this->setUpWritableCalendars(); + + $builder = $this->createMock(ICalendarEventBuilder::class); + $builder->expects($this->once())->method('setOrganizer') + ->with('alice@nextcloud.example'); + $this->eventBuilderWith($builder); + $this->appManager->method('isEnabledForUser')->willReturn(true); + $this->urlGenerator->method('linkToRoute')->willReturn('/apps/calendar/'); + + $c = $this->newController(); + + $args = $this->validCreateArgs(); + $args[] = ''; + + $response = $c->create(...$args); + + $this->assertInstanceOf(RedirectResponse::class, $response); + } + /* * create(): happy path redirects to the Calendar app */