Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion apps/dav/appinfo/info.xml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
<name>WebDAV</name>
<summary>WebDAV endpoint</summary>
<description>WebDAV endpoint</description>
<version>3.0.0-dev.0</version>
<version>3.0.0-dev.1</version>
<licence>agpl</licence>
<author>owncloud.org</author>
<namespace>DAV</namespace>
Expand Down
1 change: 1 addition & 0 deletions apps/dav/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -415,6 +415,7 @@
'OCA\\DAV\\Migration\\Version1038Date20260302000000' => $baseDir . '/../lib/Migration/Version1038Date20260302000000.php',
'OCA\\DAV\\Migration\\Version1039Date20260408000000' => $baseDir . '/../lib/Migration/Version1039Date20260408000000.php',
'OCA\\DAV\\Migration\\Version1040Date20260805000000' => $baseDir . '/../lib/Migration/Version1040Date20260805000000.php',
'OCA\\DAV\\Migration\\Version3000Date20260828000000' => $baseDir . '/../lib/Migration/Version3000Date20260828000000.php',
'OCA\\DAV\\Model\\ExampleEvent' => $baseDir . '/../lib/Model/ExampleEvent.php',
'OCA\\DAV\\Paginate\\LimitedCopyIterator' => $baseDir . '/../lib/Paginate/LimitedCopyIterator.php',
'OCA\\DAV\\Paginate\\PaginateCache' => $baseDir . '/../lib/Paginate/PaginateCache.php',
Expand Down
1 change: 1 addition & 0 deletions apps/dav/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -430,6 +430,7 @@ class ComposerStaticInitDAV
'OCA\\DAV\\Migration\\Version1038Date20260302000000' => __DIR__ . '/..' . '/../lib/Migration/Version1038Date20260302000000.php',
'OCA\\DAV\\Migration\\Version1039Date20260408000000' => __DIR__ . '/..' . '/../lib/Migration/Version1039Date20260408000000.php',
'OCA\\DAV\\Migration\\Version1040Date20260805000000' => __DIR__ . '/..' . '/../lib/Migration/Version1040Date20260805000000.php',
'OCA\\DAV\\Migration\\Version3000Date20260828000000' => __DIR__ . '/..' . '/../lib/Migration/Version3000Date20260828000000.php',
'OCA\\DAV\\Model\\ExampleEvent' => __DIR__ . '/..' . '/../lib/Model/ExampleEvent.php',
'OCA\\DAV\\Paginate\\LimitedCopyIterator' => __DIR__ . '/..' . '/../lib/Paginate/LimitedCopyIterator.php',
'OCA\\DAV\\Paginate\\PaginateCache' => __DIR__ . '/..' . '/../lib/Paginate/PaginateCache.php',
Expand Down
1 change: 1 addition & 0 deletions apps/dav/lib/CalDAV/CalDavBackend.php
Original file line number Diff line number Diff line change
Expand Up @@ -153,6 +153,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'],
];

/**
Expand Down
47 changes: 45 additions & 2 deletions apps/dav/lib/CalDAV/Reminder/Backend.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,11 @@

namespace OCA\DAV\CalDAV\Reminder;

use OCA\DAV\DAV\Sharing\Plugin;
use OCA\DAV\Db\PropertyMapper;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\IDBConnection;
use Sabre\Uri;

/**
* Class Backend
Expand All @@ -19,15 +22,23 @@
*/
class Backend {

/**
* WebDAV property a sharee sets on their own view of a shared calendar
* to mute reminders, stored via the generic custom-properties backend.
*/
private const string PROPERTY_DISABLE_ALARM_NOTIFICATIONS = '{' . Plugin::NS_NEXTCLOUD . '}disable-alarm-notifications';

/**
* Backend constructor.
*
* @param IDBConnection $db
* @param ITimeFactory $timeFactory
* @param PropertyMapper $propertyMapper
*/
public function __construct(
protected IDBConnection $db,
protected ITimeFactory $timeFactory,
private PropertyMapper $propertyMapper,
) {
}

Expand All @@ -39,12 +50,12 @@ public function __construct(
*/
public function getRemindersToProcess():array {
$query = $this->db->getQueryBuilder();
$query->select(['cr.id', 'cr.calendar_id','cr.object_id','cr.is_recurring','cr.uid','cr.recurrence_id','cr.is_recurrence_exception','cr.event_hash','cr.alarm_hash','cr.type','cr.is_relative','cr.notification_date','cr.is_repeat_based','co.calendardata', 'c.displayname', 'c.principaluri'])
$query->select(['cr.id', 'cr.calendar_id','cr.object_id','cr.is_recurring','cr.uid','cr.recurrence_id','cr.is_recurrence_exception','cr.event_hash','cr.alarm_hash','cr.type','cr.is_relative','cr.notification_date','cr.is_repeat_based','co.calendardata', 'c.displayname', 'c.principaluri', 'c.uri', 'c.disable_alarm_notifications'])
->from('calendar_reminders', 'cr')
->where($query->expr()->lte('cr.notification_date', $query->createNamedParameter($this->timeFactory->getTime())))
->join('cr', 'calendarobjects', 'co', $query->expr()->eq('cr.object_id', 'co.id'))
->join('cr', 'calendars', 'c', $query->expr()->eq('cr.calendar_id', 'c.id'))
->groupBy('cr.event_hash', 'cr.notification_date', 'cr.type', 'cr.id', 'cr.calendar_id', 'cr.object_id', 'cr.is_recurring', 'cr.uid', 'cr.recurrence_id', 'cr.is_recurrence_exception', 'cr.alarm_hash', 'cr.is_relative', 'cr.is_repeat_based', 'co.calendardata', 'c.displayname', 'c.principaluri');
->groupBy('cr.event_hash', 'cr.notification_date', 'cr.type', 'cr.id', 'cr.calendar_id', 'cr.object_id', 'cr.is_recurring', 'cr.uid', 'cr.recurrence_id', 'cr.is_recurrence_exception', 'cr.alarm_hash', 'cr.is_relative', 'cr.is_repeat_based', 'co.calendardata', 'c.displayname', 'c.principaluri', 'c.uri', 'c.disable_alarm_notifications');
$stmt = $query->executeQuery();

return array_map(
Expand All @@ -53,6 +64,35 @@ public function getRemindersToProcess():array {
);
}

/**
* Whether the given principal has muted reminders for this reminder's calendar.
*
* The calendar owner's setting is stored directly on the `calendars` row (already
* part of $reminder). A sharee's setting is their own WebDAV property on their view
* of the shared calendar, stored via the generic custom-properties backend.
*
* @param array $reminder A row as returned by getRemindersToProcess()
*/
public function isReminderMutedForPrincipal(array $reminder, string $principalUri): bool {
if ($principalUri === $reminder['principaluri']) {
return $reminder['disable_alarm_notifications'];
}

[, $shareeUid] = Uri\split($principalUri);
[, $ownerUid] = Uri\split($reminder['principaluri']);
if ($shareeUid === null || $ownerUid === null) {
return false;
}

$path = 'calendars/' . $shareeUid . '/' . $reminder['uri'] . '_shared_by_' . $ownerUid;
$properties = $this->propertyMapper->findPropertyByPathAndName($shareeUid, $path, self::PROPERTY_DISABLE_ALARM_NOTIFICATIONS);
if (empty($properties)) {
return false;
}

return $properties[0]->getPropertyvalue() === '1';
}

/**
* Get all scheduled reminders for an event
*
Expand Down Expand Up @@ -192,6 +232,9 @@ private function fixRowTyping(array $row): array {
$row['is_relative'] = (bool)$row['is_relative'];
$row['notification_date'] = (int)$row['notification_date'];
$row['is_repeat_based'] = (bool)$row['is_repeat_based'];
if (array_key_exists('disable_alarm_notifications', $row)) {
$row['disable_alarm_notifications'] = (bool)$row['disable_alarm_notifications'];
}

return $row;
}
Expand Down
23 changes: 19 additions & 4 deletions apps/dav/lib/CalDAV/Reminder/ReminderService.php
Original file line number Diff line number Diff line change
Expand Up @@ -123,15 +123,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->backend->isReminderMutedForPrincipal($reminder, $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 = [];
Expand Down
1 change: 1 addition & 0 deletions apps/dav/lib/DAV/CustomPropertiesBackend.php
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ public function propFind($path, PropFind $propFind): void {
'{http://apple.com/ns/ical/}calendar-order',
'{http://apple.com/ns/ical/}calendar-color',
'{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp',
'{http://nextcloud.com/ns}disable-alarm-notifications',
];

foreach ($customPropertiesForShares as $customPropertyForShares) {
Expand Down
40 changes: 40 additions & 0 deletions apps/dav/lib/Migration/Version3000Date20260828000000.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

declare(strict_types=1);

/**
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/

namespace OCA\DAV\Migration;

use Closure;
use OCP\DB\ISchemaWrapper;
use OCP\DB\Types;
use OCP\Migration\Attributes\AddColumn;
use OCP\Migration\Attributes\ColumnType;
use OCP\Migration\IOutput;
use OCP\Migration\SimpleMigrationStep;
use Override;

#[AddColumn(table: 'calendars', name: 'disable_alarm_notifications', type: ColumnType::BOOLEAN)]
class Version3000Date20260828000000 extends SimpleMigrationStep {
#[Override]
public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
/** @var ISchemaWrapper $schema */
$schema = $schemaClosure();
$modified = false;

$calendarsTable = $schema->getTable('calendars');
if (!$calendarsTable->hasColumn('disable_alarm_notifications')) {
$calendarsTable->addColumn('disable_alarm_notifications', Types::BOOLEAN, [
'notnull' => false,
'default' => false,
]);
$modified = true;
}

return $modified ? $schema : null;
}
}
31 changes: 31 additions & 0 deletions apps/dav/tests/unit/CalDAV/CalDavBackendTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -2478,4 +2478,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);
}
}
56 changes: 55 additions & 1 deletion apps/dav/tests/unit/CalDAV/Reminder/BackendTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,9 @@
namespace OCA\DAV\Tests\unit\CalDAV\Reminder;

use OCA\DAV\CalDAV\Reminder\Backend as ReminderBackend;
use OCA\DAV\Db\PropertyMapper;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\DB\QueryBuilder\IQueryBuilder;
use PHPUnit\Framework\MockObject\MockObject;
use Test\TestCase;

Expand All @@ -25,9 +27,10 @@ protected function setUp(): void {
$query->delete('calendar_reminders')->executeStatement();
$query->delete('calendarobjects')->executeStatement();
$query->delete('calendars')->executeStatement();
$query->delete('properties')->executeStatement();

$this->timeFactory = $this->createMock(ITimeFactory::class);
$this->reminderBackend = new ReminderBackend(self::$realDatabase, $this->timeFactory);
$this->reminderBackend = new ReminderBackend(self::$realDatabase, $this->timeFactory, new PropertyMapper(self::$realDatabase));

$this->createRemindersTestSet();
}
Expand All @@ -37,6 +40,7 @@ protected function tearDown(): void {
$query->delete('calendar_reminders')->executeStatement();
$query->delete('calendarobjects')->executeStatement();
$query->delete('calendars')->executeStatement();
$query->delete('properties')->executeStatement();

parent::tearDown();
}
Expand Down Expand Up @@ -129,6 +133,8 @@ public function testGetRemindersToProcess(): void {
'calendardata' => 'Calendar data 123',
'displayname' => 'Displayname 123',
'principaluri' => 'principals/users/user001',
'uri' => 'personal',
'disable_alarm_notifications' => false,
];
$expected2 = [
'calendar_id' => 1,
Expand All @@ -146,6 +152,8 @@ public function testGetRemindersToProcess(): void {
'calendardata' => 'Calendar data 123',
'displayname' => 'Displayname 123',
'principaluri' => 'principals/users/user001',
'uri' => 'personal',
'disable_alarm_notifications' => false,
];

$this->assertEqualsCanonicalizing([$rows[0],$rows[1]], [$expected1,$expected2]);
Expand Down Expand Up @@ -252,13 +260,58 @@ public function testUpdateReminder(): void {
$this->assertEquals((int)$row['notification_date'], 123700);
}

public function testIsReminderMutedForPrincipalUsesCalendarColumnForOwner(): void {
$reminder = [
'principaluri' => 'principals/users/user001',
'uri' => 'personal',
'disable_alarm_notifications' => true,
];

$this->assertTrue($this->reminderBackend->isReminderMutedForPrincipal($reminder, 'principals/users/user001'));
}

public function testIsReminderMutedForPrincipalIsFalseForShareeWithoutProperty(): void {
$reminder = [
'principaluri' => 'principals/users/user001',
'uri' => 'personal',
'disable_alarm_notifications' => false,
];

$this->assertFalse($this->reminderBackend->isReminderMutedForPrincipal($reminder, 'principals/users/user002'));
}

public function testIsReminderMutedForPrincipalReadsShareePropertyOverride(): void {
$reminder = [
'principaluri' => 'principals/users/user001',
'uri' => 'personal',
// Owner has not muted their own calendar ...
'disable_alarm_notifications' => false,
];
// ... but the sharee muted their own view of it.
$path = 'calendars/user002/personal_shared_by_user001';

$query = self::$realDatabase->getQueryBuilder();
$query->insert('properties')
->values([
'userid' => $query->createNamedParameter('user002'),
'propertypath' => $query->createNamedParameter($path),
'propertyname' => $query->createNamedParameter('{http://nextcloud.com/ns}disable-alarm-notifications'),
'propertyvalue' => $query->createNamedParameter('1'),
'valuetype' => $query->createNamedParameter(1, IQueryBuilder::PARAM_INT),
])
->executeStatement();

$this->assertTrue($this->reminderBackend->isReminderMutedForPrincipal($reminder, 'principals/users/user002'));
}

private function createRemindersTestSet(): void {
$query = self::$realDatabase->getQueryBuilder();
$query->insert('calendars')
->values([
'id' => $query->createNamedParameter(1),
'principaluri' => $query->createNamedParameter('principals/users/user001'),
'displayname' => $query->createNamedParameter('Displayname 123'),
'uri' => $query->createNamedParameter('personal'),
])
->executeStatement();

Expand All @@ -268,6 +321,7 @@ private function createRemindersTestSet(): void {
'id' => $query->createNamedParameter(99),
'principaluri' => $query->createNamedParameter('principals/users/user002'),
'displayname' => $query->createNamedParameter('Displayname 99'),
'uri' => $query->createNamedParameter('personal'),
])
->executeStatement();

Expand Down
Loading
Loading