fix(caldav): show confidential event if writable

If a party can edit the calendar/event, just display it instead of
hiding the details and risking overwrites.
This might be considered a change impacting privacy,
but it actually improves semantics.

Relevant test updates included, improving assertion correctness.

I think all the relevant use cases are solved by this.

Closes https://github.com/nextcloud/server/issues/5551
Closes https://github.com/nextcloud/calendar/issues/4044
Closes https://github.com/nextcloud/server/issues/11214

Signed-off-by: Arusekk <floss@arusekk.pl>
Signed-off-by: Richard Steinmetz <richard@steinmetz.cloud>
pull/54676/head
Arusekk 2025-07-17 12:25:05 +07:00 committed by Richard Steinmetz
parent 7ce484e7e8
commit 47576d10da
No known key found for this signature in database
GPG Key ID: 27137D9E7D273FB2
5 changed files with 281 additions and 17 deletions

@ -52,7 +52,8 @@ class CalendarObject extends \Sabre\CalDAV\CalendarObject {
}
// shows as busy if event is declared confidential
if ($this->objectData['classification'] === CalDavBackend::CLASSIFICATION_CONFIDENTIAL) {
if ($this->objectData['classification'] === CalDavBackend::CLASSIFICATION_CONFIDENTIAL
&& ($this->isPublic() || !$this->canWrite())) {
$this->createConfidentialObject($vObject);
}
@ -134,6 +135,10 @@ class CalendarObject extends \Sabre\CalDAV\CalendarObject {
return true;
}
private function isPublic(): bool {
return $this->calendarInfo['{http://owncloud.org/ns}public'] ?? false;
}
public function getCalendarId(): int {
return (int)$this->objectData['calendarid'];
}

@ -0,0 +1,139 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Tests\unit\CalDAV;
use OCA\DAV\CalDAV\CalDavBackend;
use OCA\DAV\CalDAV\CalendarObject;
use OCP\IL10N;
use PHPUnit\Framework\MockObject\MockObject;
use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\Component\VEvent;
use Sabre\VObject\Reader as VObjectReader;
use Test\TestCase;
class CalendarObjectTest extends TestCase {
private readonly CalDavBackend&MockObject $calDavBackend;
private readonly IL10N&MockObject $l10n;
protected function setUp(): void {
parent::setUp();
$this->calDavBackend = $this->createMock(CalDavBackend::class);
$this->l10n = $this->createMock(IL10N::class);
$this->l10n->method('t')
->willReturnArgument(0);
}
public static function provideConfidentialObjectData(): array {
return [
// Shared writable
[
false,
[
'principaluri' => 'user1',
'{http://owncloud.org/ns}owner-principal' => 'user2',
],
],
[
false,
[
'principaluri' => 'user1',
'{http://owncloud.org/ns}owner-principal' => 'user2',
'{http://owncloud.org/ns}read-only' => 0,
],
],
[
false,
[
'principaluri' => 'user1',
'{http://owncloud.org/ns}owner-principal' => 'user2',
'{http://owncloud.org/ns}read-only' => false,
],
],
// Shared read-only
[
true,
[
'principaluri' => 'user1',
'{http://owncloud.org/ns}owner-principal' => 'user2',
'{http://owncloud.org/ns}read-only' => 1,
],
],
[
true,
[
'principaluri' => 'user1',
'{http://owncloud.org/ns}owner-principal' => 'user2',
'{http://owncloud.org/ns}read-only' => true,
],
],
];
}
/**
* @dataProvider provideConfidentialObjectData
*/
public function testGetWithConfidentialObject(
bool $expectConfidential,
array $calendarInfo,
): void {
$ics = <<<EOF
BEGIN:VCALENDAR
CALSCALE:GREGORIAN
VERSION:2.0
PRODID:-//IDN nextcloud.com//Calendar app 5.5.0-dev.1//EN
BEGIN:VEVENT
CREATED:20250820T102647Z
DTSTAMP:20250820T103038Z
LAST-MODIFIED:20250820T103038Z
SEQUENCE:4
UID:a0f55f1f-4f0e-4db8-a54b-1e8b53846591
DTSTART;TZID=Europe/Berlin:20250822T110000
DTEND;TZID=Europe/Berlin:20250822T170000
STATUS:CONFIRMED
SUMMARY:confidential-event
CLASS:CONFIDENTIAL
LOCATION:A location
DESCRIPTION:A description
END:VEVENT
END:VCALENDAR
EOF;
VObjectReader::read($ics);
$calendarObject = new CalendarObject(
$this->calDavBackend,
$this->l10n,
$calendarInfo,
[
'uri' => 'a0f55f1f-4f0e-4db8-a54b-1e8b53846591.ics',
'calendardata' => $ics,
'classification' => 2, // CalDavBackend::CLASSIFICATION_CONFIDENTIAL
],
);
$actualIcs = $calendarObject->get();
$vObject = VObjectReader::read($actualIcs);
$this->assertInstanceOf(VCalendar::class, $vObject);
$vEvent = $vObject->getBaseComponent('VEVENT');
$this->assertInstanceOf(VEvent::class, $vEvent);
if ($expectConfidential) {
$this->assertEquals('Busy', $vEvent->SUMMARY?->getValue());
$this->assertNull($vEvent->DESCRIPTION);
$this->assertNull($vEvent->LOCATION);
} else {
$this->assertEquals('confidential-event', $vEvent->SUMMARY?->getValue());
$this->assertNotNull($vEvent->DESCRIPTION);
$this->assertNotNull($vEvent->LOCATION);
}
}
}

@ -310,9 +310,9 @@ class CalendarTest extends TestCase {
}
$c = new Calendar($backend, $calendarInfo, $this->l10n, $this->config, $this->logger);
$children = $c->getChildren();
$this->assertEquals($expectedChildren, count($children));
$this->assertCount($expectedChildren, $children);
$children = $c->getMultipleChildren(['event-0', 'event-1', 'event-2']);
$this->assertEquals($expectedChildren, count($children));
$this->assertCount($expectedChildren, $children);
$this->assertEquals(!$isShared, $c->childExists('event-2'));
}
@ -392,9 +392,13 @@ EOD;
'id' => 666,
'uri' => 'cal',
];
if ($isShared) {
$calendarInfo['{http://owncloud.org/ns}read-only'] = true;
}
$c = new Calendar($backend, $calendarInfo, $this->l10n, $this->config, $this->logger);
$this->assertEquals(count($c->getChildren()), $expectedChildren);
$this->assertCount($expectedChildren, $c->getChildren());
// test private event
$privateEvent = $c->getChild('event-1');
@ -599,24 +603,24 @@ EOD;
$this->assertCount(2, $roCalendar->getChildren());
// calendar data shall not be altered for the owner
$this->assertEquals($ownerCalendar->getChild('event-0')->get(), $publicObjectData);
$this->assertEquals($ownerCalendar->getChild('event-1')->get(), $confidentialObjectData);
$this->assertEquals($publicObjectData, $ownerCalendar->getChild('event-0')->get());
$this->assertEquals($confidentialObjectData, $ownerCalendar->getChild('event-1')->get());
// valarms shall not be removed for read-write shares
$this->assertEquals(
$this->fixLinebreak($rwCalendar->getChild('event-0')->get()),
$this->fixLinebreak($publicObjectData));
$this->fixLinebreak($publicObjectData),
$this->fixLinebreak($rwCalendar->getChild('event-0')->get()));
$this->assertEquals(
$this->fixLinebreak($rwCalendar->getChild('event-1')->get()),
$this->fixLinebreak($confidentialObjectCleaned));
$this->fixLinebreak($confidentialObjectData),
$this->fixLinebreak($rwCalendar->getChild('event-1')->get()));
// valarms shall be removed for read-only shares
$this->assertEquals(
$this->fixLinebreak($roCalendar->getChild('event-0')->get()),
$this->fixLinebreak($publicObjectDataWithoutVAlarm));
$this->fixLinebreak($publicObjectDataWithoutVAlarm),
$this->fixLinebreak($roCalendar->getChild('event-0')->get()));
$this->assertEquals(
$this->fixLinebreak($roCalendar->getChild('event-1')->get()),
$this->fixLinebreak($confidentialObjectCleaned));
$this->fixLinebreak($confidentialObjectCleaned),
$this->fixLinebreak($roCalendar->getChild('event-1')->get()));
}
private function fixLinebreak($str) {

@ -0,0 +1,115 @@
<?php
declare(strict_types=1);
/**
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
namespace OCA\DAV\Tests\unit\CalDAV;
use OCA\DAV\CalDAV\CalDavBackend;
use OCA\DAV\CalDAV\PublicCalendarObject;
use OCP\IL10N;
use PHPUnit\Framework\MockObject\MockObject;
use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\Component\VEvent;
use Sabre\VObject\Reader as VObjectReader;
use Test\TestCase;
class PublicCalendarObjectTest extends TestCase {
private readonly CalDavBackend&MockObject $calDavBackend;
private readonly IL10N&MockObject $l10n;
protected function setUp(): void {
parent::setUp();
$this->calDavBackend = $this->createMock(CalDavBackend::class);
$this->l10n = $this->createMock(IL10N::class);
$this->l10n->method('t')
->willReturnArgument(0);
}
public static function provideConfidentialObjectData(): array {
// For some reason, the CalDavBackend always sets read-only to false. Hence, we test for
// both cases as the property should not matter anyway.
// Ref \OCA\DAV\CalDAV\CalDavBackend::getPublicCalendars (approximately in line 538)
return [
[
[
'{http://owncloud.org/ns}read-only' => true,
'{http://owncloud.org/ns}public' => true,
],
],
[
[
'{http://owncloud.org/ns}read-only' => false,
'{http://owncloud.org/ns}public' => true,
],
],
[
[
'{http://owncloud.org/ns}read-only' => 1,
'{http://owncloud.org/ns}public' => true,
],
],
[
[
'{http://owncloud.org/ns}read-only' => 0,
'{http://owncloud.org/ns}public' => true,
],
],
];
}
/**
* @dataProvider provideConfidentialObjectData
*/
public function testGetWithConfidentialObject(array $calendarInfo): void {
$ics = <<<EOF
BEGIN:VCALENDAR
CALSCALE:GREGORIAN
VERSION:2.0
PRODID:-//IDN nextcloud.com//Calendar app 5.5.0-dev.1//EN
BEGIN:VEVENT
CREATED:20250820T102647Z
DTSTAMP:20250820T103038Z
LAST-MODIFIED:20250820T103038Z
SEQUENCE:4
UID:a0f55f1f-4f0e-4db8-a54b-1e8b53846591
DTSTART;TZID=Europe/Berlin:20250822T110000
DTEND;TZID=Europe/Berlin:20250822T170000
STATUS:CONFIRMED
SUMMARY:confidential-event
CLASS:CONFIDENTIAL
LOCATION:A location
DESCRIPTION:A description
END:VEVENT
END:VCALENDAR
EOF;
$calendarObject = new PublicCalendarObject(
$this->calDavBackend,
$this->l10n,
$calendarInfo,
[
'uri' => 'a0f55f1f-4f0e-4db8-a54b-1e8b53846591.ics',
'calendardata' => $ics,
'classification' => 2, // CalDavBackend::CLASSIFICATION_CONFIDENTIAL
],
);
$actualIcs = $calendarObject->get();
$vObject = VObjectReader::read($actualIcs);
$this->assertInstanceOf(VCalendar::class, $vObject);
$vEvent = $vObject->getBaseComponent('VEVENT');
$this->assertInstanceOf(VEvent::class, $vEvent);
$this->assertEquals('Busy', $vEvent->SUMMARY?->getValue());
$this->assertNull($vEvent->DESCRIPTION);
$this->assertNull($vEvent->LOCATION);
}
}

@ -50,9 +50,9 @@ class PublicCalendarTest extends CalendarTest {
$logger = $this->createMock(LoggerInterface::class);
$c = new PublicCalendar($backend, $calendarInfo, $this->l10n, $config, $logger);
$children = $c->getChildren();
$this->assertEquals(2, count($children));
$this->assertCount(2, $children);
$children = $c->getMultipleChildren(['event-0', 'event-1', 'event-2']);
$this->assertEquals(2, count($children));
$this->assertCount(2, $children);
$this->assertFalse($c->childExists('event-2'));
}
@ -131,6 +131,7 @@ EOD;
'principaluri' => 'user2',
'id' => 666,
'uri' => 'cal',
'{http://owncloud.org/ns}public' => true,
];
/** @var MockObject | IConfig $config */
$config = $this->createMock(IConfig::class);
@ -138,7 +139,7 @@ EOD;
$logger = $this->createMock(LoggerInterface::class);
$c = new PublicCalendar($backend, $calendarInfo, $this->l10n, $config, $logger);
$this->assertEquals(count($c->getChildren()), 2);
$this->assertCount(2, $c->getChildren());
// test private event
$privateEvent = $c->getChild('event-1');