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
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 @@ -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',
Expand Down
5 changes: 3 additions & 2 deletions apps/dav/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -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',
),
Expand Down Expand Up @@ -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',
Expand Down
73 changes: 66 additions & 7 deletions apps/dav/lib/CalDAV/CalDavBackend.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 . '}ignore-reminders' => ['ignore_reminders', 'bool'],
];

/**
Expand Down Expand Up @@ -220,6 +222,7 @@ public function __construct(
private FederatedCalendarMapper $federatedCalendarMapper,
ICacheFactory $cacheFactory,
private bool $legacyEndpoint = false,
private ?IUserSession $userSession = null,
) {
$this->publishStatusCache = $cacheFactory->createInMemory();
}
Expand Down Expand Up @@ -383,6 +386,9 @@ public function getCalendarsForUser($principalUri) {

$fields = array_column($this->propertyMap, 0);
$fields = array_map(function (string $field) {
if ($field === 'ignore_reminders') {
return 's.ignore_reminders';
}
return 'a.' . $field;
}, $fields);
$fields[] = 'a.id';
Expand Down Expand Up @@ -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 === 'ignore_reminders') {
return 's.ignore_reminders';
}
return 'a.' . $field;
}, $fields);
$fields[] = 'a.id';
$fields[] = 'a.uri';
$fields[] = 'a.synctoken';
Expand Down Expand Up @@ -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 === 'ignore_reminders') {
return 's.ignore_reminders';
}
return 'a.' . $field;
}, $fields);
$fields[] = 'a.id';
$fields[] = 'a.uri';
$fields[] = 'a.synctoken';
Expand Down Expand Up @@ -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['ignore_reminders'])) {
$rawVal = $newValues['ignore_reminders'];
if (\is_bool($rawVal)) {
$ignoreRemindersVal = $rawVal ? 1 : 0;
} elseif (\is_string($rawVal)) {
$ignoreRemindersVal = \in_array(strtolower(trim($rawVal)), ['1', 'true', 'yes'], true) ? 1 : 0;
} else {
$ignoreRemindersVal = (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('ignore_reminders', $qbUpdate->createNamedParameter($ignoreRemindersVal))
->where($qbUpdate->expr()->eq('id', $qbUpdate->createNamedParameter($shareId)));
$qbUpdate->executeStatement();
unset($newValues['ignore_reminders']);
} else {
$newValues['ignore_reminders'] = $ignoreRemindersVal;
}
} else {
$newValues['ignore_reminders'] = $ignoreRemindersVal;
}
}

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);

Expand Down
56 changes: 52 additions & 4 deletions apps/dav/lib/CalDAV/Reminder/ReminderService.php
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -58,6 +59,7 @@ public function __construct(
private IConfig $config,
private LoggerInterface $logger,
private Principal $principalConnector,
private ?IDBConnection $db = null,
) {
}

Expand Down Expand Up @@ -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 = [];
Expand Down Expand Up @@ -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('ignore_reminders')
->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('ignore_reminders')
->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;
}
}
50 changes: 50 additions & 0 deletions apps/dav/lib/Migration/Version1038Date20260828000000.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<?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: 'ignore_reminders', type: ColumnType::BOOLEAN)]
#[AddColumn(table: 'dav_shares', name: 'ignore_reminders', type: ColumnType::BOOLEAN)]
class Version1038Date20260828000000 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('ignore_reminders')) {
$calendarsTable->addColumn('ignore_reminders', Types::BOOLEAN, [
'notnull' => false,
'default' => false,
]);
$modified = true;
}

$davSharesTable = $schema->getTable('dav_shares');
if (!$davSharesTable->hasColumn('ignore_reminders')) {
$davSharesTable->addColumn('ignore_reminders', 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 @@ -2477,4 +2477,35 @@ public function testGetFederatedCalendarByUriHidesPendingCalendar(): void {

$this->assertNull($this->backend->getFederatedCalendarByUri(self::UNIT_TEST_USER, 'federated-cal'));
}

public function testIgnoreRemindersProperty(): void {
$calendarId = $this->backend->createCalendar(self::UNIT_TEST_USER, 'IgnoreRemindersTest', []);

// Default should be false
$calendars = $this->backend->getCalendarsForUser(self::UNIT_TEST_USER);
$this->assertFalse((bool)($calendars[0]['{http://nextcloud.com/ns}ignore-reminders'] ?? false));

// Update to true ('1')
$patch = new PropPatch([
'{http://nextcloud.com/ns}ignore-reminders' => '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}ignore-reminders'] ?? false));

// Update to false ('0')
$patch = new PropPatch([
'{http://nextcloud.com/ns}ignore-reminders' => '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}ignore-reminders'] ?? false));

// Clean up
$this->backend->deleteCalendar($calendars[0]['id'], true);
}
}
Loading