diff --git a/apps/dav/composer/composer/autoload_classmap.php b/apps/dav/composer/composer/autoload_classmap.php index fbe1e27bbc5c4..8beac58ec8785 100644 --- a/apps/dav/composer/composer/autoload_classmap.php +++ b/apps/dav/composer/composer/autoload_classmap.php @@ -413,6 +413,7 @@ 'OCA\\DAV\\Migration\\Version1034Date20250813093701' => $baseDir . '/../lib/Migration/Version1034Date20250813093701.php', 'OCA\\DAV\\Migration\\Version1036Date20251202000000' => $baseDir . '/../lib/Migration/Version1036Date20251202000000.php', 'OCA\\DAV\\Migration\\Version1038Date20260302000000' => $baseDir . '/../lib/Migration/Version1038Date20260302000000.php', + 'OCA\\DAV\\Migration\\Version1038Date20260828000000' => $baseDir . '/../lib/Migration/Version1038Date20260828000000.php', 'OCA\\DAV\\Migration\\Version1039Date20260408000000' => $baseDir . '/../lib/Migration/Version1039Date20260408000000.php', 'OCA\\DAV\\Migration\\Version1040Date20260805000000' => $baseDir . '/../lib/Migration/Version1040Date20260805000000.php', 'OCA\\DAV\\Model\\ExampleEvent' => $baseDir . '/../lib/Model/ExampleEvent.php', diff --git a/apps/dav/composer/composer/autoload_static.php b/apps/dav/composer/composer/autoload_static.php index 5e0385de4eddc..401ba5fcbdf45 100644 --- a/apps/dav/composer/composer/autoload_static.php +++ b/apps/dav/composer/composer/autoload_static.php @@ -7,14 +7,14 @@ class ComposerStaticInitDAV { public static $prefixLengthsPsr4 = array ( - 'O' => + 'O' => array ( 'OCA\\DAV\\' => 8, ), ); public static $prefixDirsPsr4 = array ( - 'OCA\\DAV\\' => + 'OCA\\DAV\\' => array ( 0 => __DIR__ . '/..' . '/../lib', ), @@ -428,6 +428,7 @@ class ComposerStaticInitDAV 'OCA\\DAV\\Migration\\Version1034Date20250813093701' => __DIR__ . '/..' . '/../lib/Migration/Version1034Date20250813093701.php', 'OCA\\DAV\\Migration\\Version1036Date20251202000000' => __DIR__ . '/..' . '/../lib/Migration/Version1036Date20251202000000.php', 'OCA\\DAV\\Migration\\Version1038Date20260302000000' => __DIR__ . '/..' . '/../lib/Migration/Version1038Date20260302000000.php', + 'OCA\\DAV\\Migration\\Version1038Date20260828000000' => __DIR__ . '/..' . '/../lib/Migration/Version1038Date20260828000000.php', 'OCA\\DAV\\Migration\\Version1039Date20260408000000' => __DIR__ . '/..' . '/../lib/Migration/Version1039Date20260408000000.php', 'OCA\\DAV\\Migration\\Version1040Date20260805000000' => __DIR__ . '/..' . '/../lib/Migration/Version1040Date20260805000000.php', 'OCA\\DAV\\Model\\ExampleEvent' => __DIR__ . '/..' . '/../lib/Model/ExampleEvent.php', diff --git a/apps/dav/lib/CalDAV/CalDavBackend.php b/apps/dav/lib/CalDAV/CalDavBackend.php index 86ed21795f4c1..695f4ec5293ac 100644 --- a/apps/dav/lib/CalDAV/CalDavBackend.php +++ b/apps/dav/lib/CalDAV/CalDavBackend.php @@ -50,6 +50,7 @@ use OCP\IConfig; use OCP\IDBConnection; use OCP\IUserManager; +use OCP\IUserSession; use OCP\Security\ISecureRandom; use Psr\Log\LoggerInterface; use RuntimeException; @@ -153,6 +154,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}deleted-at' => ['deleted_at', 'int'], '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}default-alarm-part-day' => ['default_alarm_pday', 'int'], '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}default-alarm-full-day' => ['default_alarm_fday', 'int'], + '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}disable-alarm-notifications' => ['disable_alarm_notifications', 'bool'], ]; /** @@ -220,6 +222,7 @@ public function __construct( private FederatedCalendarMapper $federatedCalendarMapper, ICacheFactory $cacheFactory, private bool $legacyEndpoint = false, + private ?IUserSession $userSession = null, ) { $this->publishStatusCache = $cacheFactory->createInMemory(); } @@ -383,6 +386,9 @@ public function getCalendarsForUser($principalUri) { $fields = array_column($this->propertyMap, 0); $fields = array_map(function (string $field) { + if ($field === 'disable_alarm_notifications') { + return 's.disable_alarm_notifications'; + } return 'a.' . $field; }, $fields); $fields[] = 'a.id'; @@ -514,6 +520,12 @@ public function getUsersOwnCalendars($principalUri) { */ public function getPublicCalendars() { $fields = array_column($this->propertyMap, 0); + $fields = array_map(function (string $field) { + if ($field === 'disable_alarm_notifications') { + return 's.disable_alarm_notifications'; + } + return 'a.' . $field; + }, $fields); $fields[] = 'a.id'; $fields[] = 'a.uri'; $fields[] = 'a.synctoken'; @@ -572,6 +584,12 @@ public function getPublicCalendars() { */ public function getPublicCalendar($uri) { $fields = array_column($this->propertyMap, 0); + $fields = array_map(function (string $field) { + if ($field === 'disable_alarm_notifications') { + return 's.disable_alarm_notifications'; + } + return 'a.' . $field; + }, $fields); $fields[] = 'a.id'; $fields[] = 'a.uri'; $fields[] = 'a.synctoken'; @@ -905,14 +923,55 @@ public function updateCalendar($calendarId, PropPatch $propPatch) { break; } } - [$calendarData, $shares] = $this->atomic(function () use ($calendarId, $newValues) { - $query = $this->db->getQueryBuilder(); - $query->update('calendars'); - foreach ($newValues as $fieldName => $value) { - $query->set($fieldName, $query->createNamedParameter($value)); + [$calendarData, $shares] = $this->atomic(function () use ($calendarId, &$newValues) { + if (isset($newValues['disable_alarm_notifications'])) { + $rawVal = $newValues['disable_alarm_notifications']; + if (\is_bool($rawVal)) { + $disableAlarmNotificationsVal = $rawVal ? 1 : 0; + } elseif (\is_string($rawVal)) { + $disableAlarmNotificationsVal = \in_array(strtolower(trim($rawVal)), ['1', 'true', 'yes'], true) ? 1 : 0; + } else { + $disableAlarmNotificationsVal = (int)(bool)$rawVal; + } + + $user = $this->userSession?->getUser(); + if ($user !== null) { + $principalUri = 'principals/users/' . $user->getUID(); + $principals = $this->principalBackend->getGroupMembership($principalUri, true); + $principals = array_merge($principals, $this->principalBackend->getCircleMembership($principalUri)); + $principals[] = $principalUri; + + $qbCheck = $this->db->getQueryBuilder(); + $qbCheck->select('id') + ->from('dav_shares') + ->where($qbCheck->expr()->eq('resourceid', $qbCheck->createNamedParameter($calendarId))) + ->andWhere($qbCheck->expr()->in('principaluri', $qbCheck->createNamedParameter($principals, IQueryBuilder::PARAM_STR_ARRAY))) + ->andWhere($qbCheck->expr()->eq('type', $qbCheck->createNamedParameter('calendar'))); + $shareId = $qbCheck->executeQuery()->fetchOne(); + if ($shareId !== false) { + $qbUpdate = $this->db->getQueryBuilder(); + $qbUpdate->update('dav_shares') + ->set('disable_alarm_notifications', $qbUpdate->createNamedParameter($disableAlarmNotificationsVal)) + ->where($qbUpdate->expr()->eq('id', $qbUpdate->createNamedParameter($shareId))); + $qbUpdate->executeStatement(); + unset($newValues['disable_alarm_notifications']); + } else { + $newValues['disable_alarm_notifications'] = $disableAlarmNotificationsVal; + } + } else { + $newValues['disable_alarm_notifications'] = $disableAlarmNotificationsVal; + } + } + + if (!empty($newValues)) { + $query = $this->db->getQueryBuilder(); + $query->update('calendars'); + foreach ($newValues as $fieldName => $value) { + $query->set($fieldName, $query->createNamedParameter($value)); + } + $query->where($query->expr()->eq('id', $query->createNamedParameter($calendarId))); + $query->executeStatement(); } - $query->where($query->expr()->eq('id', $query->createNamedParameter($calendarId))); - $query->executeStatement(); $this->addChanges($calendarId, [''], 2); diff --git a/apps/dav/lib/CalDAV/Reminder/ReminderService.php b/apps/dav/lib/CalDAV/Reminder/ReminderService.php index ef480a310b2b6..83cfc7cf9720a 100644 --- a/apps/dav/lib/CalDAV/Reminder/ReminderService.php +++ b/apps/dav/lib/CalDAV/Reminder/ReminderService.php @@ -15,6 +15,7 @@ use OCA\DAV\Connector\Sabre\Principal; use OCP\AppFramework\Utility\ITimeFactory; use OCP\IConfig; +use OCP\IDBConnection; use OCP\IGroup; use OCP\IGroupManager; use OCP\IUser; @@ -58,6 +59,7 @@ public function __construct( private IConfig $config, private LoggerInterface $logger, private Principal $principalConnector, + private ?IDBConnection $db = null, ) { } @@ -123,15 +125,30 @@ public function processReminders() :void { continue; } - if ($this->config->getAppValue('dav', 'sendEventRemindersToSharedUsers', 'yes') === 'no') { + if ($this->config->getAppValue('dav', 'sendEventRemindersToSharedUsers', 'yes') === 'yes') { $users = $this->getAllUsersWithWriteAccessToCalendar($reminder['calendar_id']); } else { $users = []; } - $user = $this->getUserFromPrincipalURI($reminder['principaluri']); - if ($user) { - $users[] = $user; + $ownerUser = $this->getUserFromPrincipalURI($reminder['principaluri']); + if ($ownerUser !== null) { + $users[] = $ownerUser; + } + + // Filter out any users who have muted reminders for this calendar + $users = array_values(array_filter($users, function (IUser $u) use ($reminder): bool { + $principalUri = 'principals/users/' . $u->getUID(); + return !$this->isReminderIgnored((int)$reminder['calendar_id'], $principalUri); + })); + + if (count($users) === 0) { + $this->logger->debug('Reminder {id} is ignored by all recipient users for calendar {calendarId}', [ + 'id' => $reminder['id'], + 'calendarId' => $reminder['calendar_id'], + ]); + $this->deleteOrProcessNext($reminder, $vevent); + continue; } $userPrincipalEmailAddresses = []; @@ -834,4 +851,35 @@ private function getCalendarTimeZone(int $calendarid): DateTimeZone { $vtimezone = $vtimezoneObj->VTIMEZONE; return $vtimezone->getTimeZone(); } + + private function isReminderIgnored(int $calendarId, string $principalUri): bool { + if ($this->db === null) { + return false; + } + + // 1. Check dav_shares for sharee setting + $qbShares = $this->db->getQueryBuilder(); + $qbShares->select('disable_alarm_notifications') + ->from('dav_shares') + ->where($qbShares->expr()->eq('resourceid', $qbShares->createNamedParameter($calendarId))) + ->andWhere($qbShares->expr()->eq('principaluri', $qbShares->createNamedParameter($principalUri))) + ->andWhere($qbShares->expr()->eq('type', $qbShares->createNamedParameter('calendar'))); + $ignoredShare = $qbShares->executeQuery()->fetchOne(); + if ($ignoredShare !== false) { + return (bool)$ignoredShare; + } + + // 2. Check calendars table for owner setting + $qbCal = $this->db->getQueryBuilder(); + $qbCal->select('disable_alarm_notifications') + ->from('calendars') + ->where($qbCal->expr()->eq('id', $qbCal->createNamedParameter($calendarId))) + ->andWhere($qbCal->expr()->eq('principaluri', $qbCal->createNamedParameter($principalUri))); + $ignoredCal = $qbCal->executeQuery()->fetchOne(); + if ($ignoredCal !== false) { + return (bool)$ignoredCal; + } + + return false; + } } diff --git a/apps/dav/lib/Migration/Version1038Date20260828000000.php b/apps/dav/lib/Migration/Version1038Date20260828000000.php new file mode 100644 index 0000000000000..6890317d323c3 --- /dev/null +++ b/apps/dav/lib/Migration/Version1038Date20260828000000.php @@ -0,0 +1,50 @@ +getTable('calendars'); + if (!$calendarsTable->hasColumn('disable_alarm_notifications')) { + $calendarsTable->addColumn('disable_alarm_notifications', Types::BOOLEAN, [ + 'notnull' => false, + 'default' => false, + ]); + $modified = true; + } + + $davSharesTable = $schema->getTable('dav_shares'); + if (!$davSharesTable->hasColumn('disable_alarm_notifications')) { + $davSharesTable->addColumn('disable_alarm_notifications', Types::BOOLEAN, [ + 'notnull' => false, + 'default' => false, + ]); + $modified = true; + } + + return $modified ? $schema : null; + } +} diff --git a/apps/dav/tests/unit/CalDAV/CalDavBackendTest.php b/apps/dav/tests/unit/CalDAV/CalDavBackendTest.php index 75bd0f118906f..9a154696a483b 100644 --- a/apps/dav/tests/unit/CalDAV/CalDavBackendTest.php +++ b/apps/dav/tests/unit/CalDAV/CalDavBackendTest.php @@ -2477,4 +2477,35 @@ public function testGetFederatedCalendarByUriHidesPendingCalendar(): void { $this->assertNull($this->backend->getFederatedCalendarByUri(self::UNIT_TEST_USER, 'federated-cal')); } + + public function testDisableAlarmNotificationsProperty(): void { + $calendarId = $this->backend->createCalendar(self::UNIT_TEST_USER, 'DisableAlarmNotificationsTest', []); + + // Default should be false + $calendars = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER); + $this->assertFalse((bool)($calendars[0]['{http://nextcloud.com/ns}disable-alarm-notifications'] ?? false)); + + // Update to true ('1') + $patch = new PropPatch([ + '{http://nextcloud.com/ns}disable-alarm-notifications' => '1', + ]); + $this->backend->updateCalendar($calendarId, $patch); + $patch->commit(); + + $calendars = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER); + $this->assertTrue((bool)($calendars[0]['{http://nextcloud.com/ns}disable-alarm-notifications'] ?? false)); + + // Update to false ('0') + $patch = new PropPatch([ + '{http://nextcloud.com/ns}disable-alarm-notifications' => '0', + ]); + $this->backend->updateCalendar($calendarId, $patch); + $patch->commit(); + + $calendars = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER); + $this->assertFalse((bool)($calendars[0]['{http://nextcloud.com/ns}disable-alarm-notifications'] ?? false)); + + // Clean up + $this->backend->deleteCalendar($calendars[0]['id'], true); + } }