Skip to content

Commit 8cc0796

Browse files
Arusekkbackportbot[bot]
authored andcommitted
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 #5551 Closes nextcloud/calendar#4044 Closes #11214 Signed-off-by: Arusekk <floss@arusekk.pl> Signed-off-by: Richard Steinmetz <richard@steinmetz.cloud>
1 parent d0db7d6 commit 8cc0796

File tree

5 files changed

+279
-17
lines changed

5 files changed

+279
-17
lines changed

apps/dav/lib/CalDAV/CalendarObject.php

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,7 +55,8 @@ public function get() {
5555
}
5656

5757
// shows as busy if event is declared confidential
58-
if ($this->objectData['classification'] === CalDavBackend::CLASSIFICATION_CONFIDENTIAL) {
58+
if ($this->objectData['classification'] === CalDavBackend::CLASSIFICATION_CONFIDENTIAL
59+
&& ($this->isPublic() || !$this->canWrite())) {
5960
$this->createConfidentialObject($vObject);
6061
}
6162

@@ -137,6 +138,10 @@ private function canWrite() {
137138
return true;
138139
}
139140

141+
private function isPublic(): bool {
142+
return $this->calendarInfo['{http://owncloud.org/ns}public'] ?? false;
143+
}
144+
140145
public function getCalendarId(): int {
141146
return (int) $this->objectData['calendarid'];
142147
}
Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\DAV\Tests\unit\CalDAV;
11+
12+
use OCA\DAV\CalDAV\CalDavBackend;
13+
use OCA\DAV\CalDAV\CalendarObject;
14+
use OCP\IL10N;
15+
use PHPUnit\Framework\Attributes\DataProvider;
16+
use PHPUnit\Framework\MockObject\MockObject;
17+
use Sabre\VObject\Component\VCalendar;
18+
use Sabre\VObject\Component\VEvent;
19+
use Sabre\VObject\Reader as VObjectReader;
20+
use Test\TestCase;
21+
22+
class CalendarObjectTest extends TestCase {
23+
private readonly CalDavBackend&MockObject $calDavBackend;
24+
private readonly IL10N&MockObject $l10n;
25+
26+
protected function setUp(): void {
27+
parent::setUp();
28+
29+
$this->calDavBackend = $this->createMock(CalDavBackend::class);
30+
$this->l10n = $this->createMock(IL10N::class);
31+
32+
$this->l10n->method('t')
33+
->willReturnArgument(0);
34+
}
35+
36+
public static function provideConfidentialObjectData(): array {
37+
return [
38+
// Shared writable
39+
[
40+
false,
41+
[
42+
'principaluri' => 'user1',
43+
'{http://owncloud.org/ns}owner-principal' => 'user2',
44+
],
45+
],
46+
[
47+
false,
48+
[
49+
'principaluri' => 'user1',
50+
'{http://owncloud.org/ns}owner-principal' => 'user2',
51+
'{http://owncloud.org/ns}read-only' => 0,
52+
],
53+
],
54+
[
55+
false,
56+
[
57+
'principaluri' => 'user1',
58+
'{http://owncloud.org/ns}owner-principal' => 'user2',
59+
'{http://owncloud.org/ns}read-only' => false,
60+
],
61+
],
62+
// Shared read-only
63+
[
64+
true,
65+
[
66+
'principaluri' => 'user1',
67+
'{http://owncloud.org/ns}owner-principal' => 'user2',
68+
'{http://owncloud.org/ns}read-only' => 1,
69+
],
70+
],
71+
[
72+
true,
73+
[
74+
'principaluri' => 'user1',
75+
'{http://owncloud.org/ns}owner-principal' => 'user2',
76+
'{http://owncloud.org/ns}read-only' => true,
77+
],
78+
],
79+
];
80+
}
81+
82+
#[DataProvider('provideConfidentialObjectData')]
83+
public function testGetWithConfidentialObject(
84+
bool $expectConfidential,
85+
array $calendarInfo,
86+
): void {
87+
$ics = <<<EOF
88+
BEGIN:VCALENDAR
89+
CALSCALE:GREGORIAN
90+
VERSION:2.0
91+
PRODID:-//IDN nextcloud.com//Calendar app 5.5.0-dev.1//EN
92+
BEGIN:VEVENT
93+
CREATED:20250820T102647Z
94+
DTSTAMP:20250820T103038Z
95+
LAST-MODIFIED:20250820T103038Z
96+
SEQUENCE:4
97+
UID:a0f55f1f-4f0e-4db8-a54b-1e8b53846591
98+
DTSTART;TZID=Europe/Berlin:20250822T110000
99+
DTEND;TZID=Europe/Berlin:20250822T170000
100+
STATUS:CONFIRMED
101+
SUMMARY:confidential-event
102+
CLASS:CONFIDENTIAL
103+
LOCATION:A location
104+
DESCRIPTION:A description
105+
END:VEVENT
106+
END:VCALENDAR
107+
EOF;
108+
VObjectReader::read($ics);
109+
110+
$calendarObject = new CalendarObject(
111+
$this->calDavBackend,
112+
$this->l10n,
113+
$calendarInfo,
114+
[
115+
'uri' => 'a0f55f1f-4f0e-4db8-a54b-1e8b53846591.ics',
116+
'calendardata' => $ics,
117+
'classification' => 2, // CalDavBackend::CLASSIFICATION_CONFIDENTIAL
118+
],
119+
);
120+
121+
$actualIcs = $calendarObject->get();
122+
$vObject = VObjectReader::read($actualIcs);
123+
124+
$this->assertInstanceOf(VCalendar::class, $vObject);
125+
$vEvent = $vObject->getBaseComponent('VEVENT');
126+
$this->assertInstanceOf(VEvent::class, $vEvent);
127+
128+
if ($expectConfidential) {
129+
$this->assertEquals('Busy', $vEvent->SUMMARY?->getValue());
130+
$this->assertNull($vEvent->DESCRIPTION);
131+
$this->assertNull($vEvent->LOCATION);
132+
} else {
133+
$this->assertEquals('confidential-event', $vEvent->SUMMARY?->getValue());
134+
$this->assertNotNull($vEvent->DESCRIPTION);
135+
$this->assertNotNull($vEvent->LOCATION);
136+
}
137+
}
138+
}

apps/dav/tests/unit/CalDAV/CalendarTest.php

Lines changed: 17 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -311,9 +311,9 @@ public function testPrivateClassification($expectedChildren, $isShared): void {
311311
}
312312
$c = new Calendar($backend, $calendarInfo, $this->l10n, $this->config, $this->logger);
313313
$children = $c->getChildren();
314-
$this->assertEquals($expectedChildren, count($children));
314+
$this->assertCount($expectedChildren, $children);
315315
$children = $c->getMultipleChildren(['event-0', 'event-1', 'event-2']);
316-
$this->assertEquals($expectedChildren, count($children));
316+
$this->assertCount($expectedChildren, $children);
317317

318318
$this->assertEquals(!$isShared, $c->childExists('event-2'));
319319
}
@@ -393,9 +393,13 @@ public function testConfidentialClassification($expectedChildren, $isShared): vo
393393
'id' => 666,
394394
'uri' => 'cal',
395395
];
396+
397+
if ($isShared) {
398+
$calendarInfo['{http://owncloud.org/ns}read-only'] = true;
399+
}
396400
$c = new Calendar($backend, $calendarInfo, $this->l10n, $this->config, $this->logger);
397401

398-
$this->assertEquals(count($c->getChildren()), $expectedChildren);
402+
$this->assertCount($expectedChildren, $c->getChildren());
399403

400404
// test private event
401405
$privateEvent = $c->getChild('event-1');
@@ -600,24 +604,24 @@ public function testRemoveVAlarms() {
600604
$this->assertCount(2, $roCalendar->getChildren());
601605

602606
// calendar data shall not be altered for the owner
603-
$this->assertEquals($ownerCalendar->getChild('event-0')->get(), $publicObjectData);
604-
$this->assertEquals($ownerCalendar->getChild('event-1')->get(), $confidentialObjectData);
607+
$this->assertEquals($publicObjectData, $ownerCalendar->getChild('event-0')->get());
608+
$this->assertEquals($confidentialObjectData, $ownerCalendar->getChild('event-1')->get());
605609

606610
// valarms shall not be removed for read-write shares
607611
$this->assertEquals(
608-
$this->fixLinebreak($rwCalendar->getChild('event-0')->get()),
609-
$this->fixLinebreak($publicObjectData));
612+
$this->fixLinebreak($publicObjectData),
613+
$this->fixLinebreak($rwCalendar->getChild('event-0')->get()));
610614
$this->assertEquals(
611-
$this->fixLinebreak($rwCalendar->getChild('event-1')->get()),
612-
$this->fixLinebreak($confidentialObjectCleaned));
615+
$this->fixLinebreak($confidentialObjectData),
616+
$this->fixLinebreak($rwCalendar->getChild('event-1')->get()));
613617

614618
// valarms shall be removed for read-only shares
615619
$this->assertEquals(
616-
$this->fixLinebreak($roCalendar->getChild('event-0')->get()),
617-
$this->fixLinebreak($publicObjectDataWithoutVAlarm));
620+
$this->fixLinebreak($publicObjectDataWithoutVAlarm),
621+
$this->fixLinebreak($roCalendar->getChild('event-0')->get()));
618622
$this->assertEquals(
619-
$this->fixLinebreak($roCalendar->getChild('event-1')->get()),
620-
$this->fixLinebreak($confidentialObjectCleaned));
623+
$this->fixLinebreak($confidentialObjectCleaned),
624+
$this->fixLinebreak($roCalendar->getChild('event-1')->get()));
621625
}
622626

623627
private function fixLinebreak($str) {
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
/**
6+
* SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
7+
* SPDX-License-Identifier: AGPL-3.0-or-later
8+
*/
9+
10+
namespace OCA\DAV\Tests\unit\CalDAV;
11+
12+
use OCA\DAV\CalDAV\CalDavBackend;
13+
use OCA\DAV\CalDAV\PublicCalendarObject;
14+
use OCP\IL10N;
15+
use PHPUnit\Framework\Attributes\DataProvider;
16+
use PHPUnit\Framework\MockObject\MockObject;
17+
use Sabre\VObject\Component\VCalendar;
18+
use Sabre\VObject\Component\VEvent;
19+
use Sabre\VObject\Reader as VObjectReader;
20+
use Test\TestCase;
21+
22+
class PublicCalendarObjectTest extends TestCase {
23+
private readonly CalDavBackend&MockObject $calDavBackend;
24+
private readonly IL10N&MockObject $l10n;
25+
26+
protected function setUp(): void {
27+
parent::setUp();
28+
29+
$this->calDavBackend = $this->createMock(CalDavBackend::class);
30+
$this->l10n = $this->createMock(IL10N::class);
31+
32+
$this->l10n->method('t')
33+
->willReturnArgument(0);
34+
}
35+
36+
public static function provideConfidentialObjectData(): array {
37+
// For some reason, the CalDavBackend always sets read-only to false. Hence, we test for
38+
// both cases as the property should not matter anyway.
39+
// Ref \OCA\DAV\CalDAV\CalDavBackend::getPublicCalendars (approximately in line 538)
40+
return [
41+
[
42+
[
43+
'{http://owncloud.org/ns}read-only' => true,
44+
'{http://owncloud.org/ns}public' => true,
45+
],
46+
],
47+
[
48+
[
49+
'{http://owncloud.org/ns}read-only' => false,
50+
'{http://owncloud.org/ns}public' => true,
51+
],
52+
],
53+
[
54+
[
55+
'{http://owncloud.org/ns}read-only' => 1,
56+
'{http://owncloud.org/ns}public' => true,
57+
],
58+
],
59+
[
60+
[
61+
'{http://owncloud.org/ns}read-only' => 0,
62+
'{http://owncloud.org/ns}public' => true,
63+
],
64+
],
65+
];
66+
}
67+
68+
#[DataProvider('provideConfidentialObjectData')]
69+
public function testGetWithConfidentialObject(array $calendarInfo): void {
70+
$ics = <<<EOF
71+
BEGIN:VCALENDAR
72+
CALSCALE:GREGORIAN
73+
VERSION:2.0
74+
PRODID:-//IDN nextcloud.com//Calendar app 5.5.0-dev.1//EN
75+
BEGIN:VEVENT
76+
CREATED:20250820T102647Z
77+
DTSTAMP:20250820T103038Z
78+
LAST-MODIFIED:20250820T103038Z
79+
SEQUENCE:4
80+
UID:a0f55f1f-4f0e-4db8-a54b-1e8b53846591
81+
DTSTART;TZID=Europe/Berlin:20250822T110000
82+
DTEND;TZID=Europe/Berlin:20250822T170000
83+
STATUS:CONFIRMED
84+
SUMMARY:confidential-event
85+
CLASS:CONFIDENTIAL
86+
LOCATION:A location
87+
DESCRIPTION:A description
88+
END:VEVENT
89+
END:VCALENDAR
90+
EOF;
91+
92+
$calendarObject = new PublicCalendarObject(
93+
$this->calDavBackend,
94+
$this->l10n,
95+
$calendarInfo,
96+
[
97+
'uri' => 'a0f55f1f-4f0e-4db8-a54b-1e8b53846591.ics',
98+
'calendardata' => $ics,
99+
'classification' => 2, // CalDavBackend::CLASSIFICATION_CONFIDENTIAL
100+
],
101+
);
102+
103+
$actualIcs = $calendarObject->get();
104+
$vObject = VObjectReader::read($actualIcs);
105+
106+
$this->assertInstanceOf(VCalendar::class, $vObject);
107+
$vEvent = $vObject->getBaseComponent('VEVENT');
108+
$this->assertInstanceOf(VEvent::class, $vEvent);
109+
110+
$this->assertEquals('Busy', $vEvent->SUMMARY?->getValue());
111+
$this->assertNull($vEvent->DESCRIPTION);
112+
$this->assertNull($vEvent->LOCATION);
113+
}
114+
}

apps/dav/tests/unit/CalDAV/PublicCalendarTest.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,9 @@ public function testPrivateClassification($expectedChildren, $isShared): void {
5151
$logger = $this->createMock(LoggerInterface::class);
5252
$c = new PublicCalendar($backend, $calendarInfo, $this->l10n, $config, $logger);
5353
$children = $c->getChildren();
54-
$this->assertEquals(2, count($children));
54+
$this->assertCount(2, $children);
5555
$children = $c->getMultipleChildren(['event-0', 'event-1', 'event-2']);
56-
$this->assertEquals(2, count($children));
56+
$this->assertCount(2, $children);
5757

5858
$this->assertFalse($c->childExists('event-2'));
5959
}
@@ -132,14 +132,15 @@ public function testConfidentialClassification($expectedChildren, $isShared): vo
132132
'principaluri' => 'user2',
133133
'id' => 666,
134134
'uri' => 'cal',
135+
'{http://owncloud.org/ns}public' => true,
135136
];
136137
/** @var MockObject | IConfig $config */
137138
$config = $this->createMock(IConfig::class);
138139
/** @var MockObject | LoggerInterface $logger */
139140
$logger = $this->createMock(LoggerInterface::class);
140141
$c = new PublicCalendar($backend, $calendarInfo, $this->l10n, $config, $logger);
141142

142-
$this->assertEquals(count($c->getChildren()), 2);
143+
$this->assertCount(2, $c->getChildren());
143144

144145
// test private event
145146
$privateEvent = $c->getChild('event-1');

0 commit comments

Comments
 (0)