Skip to content

Commit a6e5e5a

Browse files
authored
[Chore] Fix public calendar behavior (#239)
1 parent d466623 commit a6e5e5a

11 files changed

Lines changed: 116 additions & 60 deletions

File tree

README.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,9 @@ Davis
44
[![Build Status][ci_badge]][ci_link]
55
[![Publish Docker image](https://github.com/tchapi/davis/actions/workflows/main.yml/badge.svg?branch=main)](https://github.com/tchapi/davis/actions/workflows/main.yml)
66
[![Latest release][release_badge]][release_link]
7+
[![License](https://img.shields.io/github/license/tchapi/davis)](https://github.com/tchapi/davis/blob/main/LICENSE)
8+
![Platform](https://img.shields.io/badge/platform-amd64%20%7C%20arm64-blue?logo=docker)
9+
![PHP Version](https://img.shields.io/badge/php-8.2%20%7C%208.3%20%7C%208.4-777BB4?logo=php&logoColor=white)
710
[![Sponsor me][sponsor_badge]][sponsor_link]
811

912
A modern, simple, feature-packed, fully translatable DAV server, admin interface and frontend based on `sabre/dav`, built with [Symfony 7](https://symfony.com/) and [Bootstrap 5](https://getbootstrap.com/), initially inspired by [Baïkal](https://github.com/sabre-io/Baikal) (_see dependencies table below for more detail_)
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace DoctrineMigrations;
6+
7+
use Doctrine\DBAL\Schema\Schema;
8+
use Doctrine\Migrations\AbstractMigration;
9+
10+
/**
11+
* Add public flag on calendar instance.
12+
*/
13+
final class Version20260131161930 extends AbstractMigration
14+
{
15+
public function getDescription(): string
16+
{
17+
return 'Add public flag on CalendarInstance (replacing ACCESS_PUBLIC flag)';
18+
}
19+
20+
public function up(Schema $schema): void
21+
{
22+
$engine = $this->connection->getDatabasePlatform()->getName();
23+
24+
if ('mysql' === $engine) {
25+
$this->addSql('ALTER TABLE calendarinstances ADD public TINYINT(1) DEFAULT 0 NOT NULL');
26+
} elseif ('postgresql' === $engine) {
27+
$this->addSql('ALTER TABLE calendarinstances ADD public BOOLEAN DEFAULT FALSE NOT NULL');
28+
} elseif ('sqlite' === $engine) {
29+
$this->addSql('ALTER TABLE calendarinstances ADD public BOOLEAN DEFAULT 0 NOT NULL');
30+
}
31+
32+
// Migrate ACCESS_PUBLIC (10) to ACCESS_SHAREDOWNER (1) + public = true
33+
if ('postgresql' === $engine) {
34+
$this->addSql('UPDATE calendarinstances SET public = TRUE, access = 1 WHERE access = 10');
35+
} else {
36+
// MySQL and SQLite accept 1/0 for booleans
37+
$this->addSql('UPDATE calendarinstances SET public = 1, access = 1 WHERE access = 10');
38+
}
39+
}
40+
41+
public function down(Schema $schema): void
42+
{
43+
$engine = $this->connection->getDatabasePlatform()->getName();
44+
45+
// Revert public = true back to ACCESS_PUBLIC (10)
46+
if ('postgresql' === $engine) {
47+
$this->addSql('UPDATE calendarinstances SET access = 10 WHERE is_public = TRUE');
48+
} else {
49+
$this->addSql('UPDATE calendarinstances SET access = 10 WHERE is_public = 1');
50+
}
51+
52+
if ('mysql' === $engine) {
53+
$this->addSql('ALTER TABLE calendarinstances DROP public');
54+
} elseif ('postgresql' === $engine) {
55+
$this->addSql('ALTER TABLE calendarinstances DROP COLUMN public');
56+
} elseif ('sqlite' === $engine) {
57+
$this->addSql('ALTER TABLE calendarinstances DROP COLUMN public');
58+
}
59+
}
60+
}

src/Controller/Admin/CalendarController.php

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
use App\Entity\SchedulingObject;
1010
use App\Form\CalendarInstanceType;
1111
use Doctrine\Persistence\ManagerRegistry;
12+
use Sabre\DAV\Sharing\Plugin as SharingPlugin;
1213
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
1314
use Symfony\Component\HttpFoundation\JsonResponse;
1415
use Symfony\Component\HttpFoundation\Request;
@@ -100,9 +101,6 @@ public function calendarEdit(ManagerRegistry $doctrine, Request $request, string
100101
$form->get('events')->setData(in_array(Calendar::COMPONENT_EVENTS, $components));
101102
$form->get('todos')->setData(in_array(Calendar::COMPONENT_TODOS, $components));
102103
$form->get('notes')->setData(in_array(Calendar::COMPONENT_NOTES, $components));
103-
if ($arePublicCalendarsEnabled) {
104-
$form->get('public')->setData($calendarInstance->isPublic());
105-
}
106104
$form->get('principalUri')->setData(Principal::PREFIX.$username);
107105

108106
$form->handleRequest($request);
@@ -123,9 +121,9 @@ public function calendarEdit(ManagerRegistry $doctrine, Request $request, string
123121
$components[] = Calendar::COMPONENT_NOTES;
124122
}
125123
if ($arePublicCalendarsEnabled && true === $form->get('public')->getData()) {
126-
$calendarInstance->setAccess(CalendarInstance::ACCESS_PUBLIC);
124+
$calendarInstance->setPublic(true);
127125
} else {
128-
$calendarInstance->setAccess(CalendarInstance::ACCESS_SHAREDOWNER);
126+
$calendarInstance->setPublic(false);
129127
}
130128

131129
$calendarInstance->getCalendar()->setComponents(implode(',', $components));
@@ -168,7 +166,7 @@ public function calendarShares(ManagerRegistry $doctrine, string $username, stri
168166
'displayName' => $instance['displayName'],
169167
'email' => $instance['email'],
170168
'accessText' => $trans->trans('calendar.share_access.'.$instance[0]['access']),
171-
'isWriteAccess' => CalendarInstance::ACCESS_READWRITE === $instance[0]['access'],
169+
'isWriteAccess' => SharingPlugin::ACCESS_READWRITE === $instance[0]['access'],
172170
'revokeUrl' => $this->generateUrl('calendar_revoke', ['username' => $username, 'id' => $instance[0]['id']]),
173171
];
174172
}
@@ -197,7 +195,7 @@ public function calendarShareAdd(ManagerRegistry $doctrine, Request $request, st
197195
// already existing first, so we can update it:
198196
$existingSharedInstance = $doctrine->getRepository(CalendarInstance::class)->findSharedInstanceOfInstanceFor($instance->getCalendar()->getId(), $newShareeToAdd->getUri());
199197

200-
$writeAccess = ('true' === $request->get('write') ? CalendarInstance::ACCESS_READWRITE : CalendarInstance::ACCESS_READ);
198+
$writeAccess = ('true' === $request->get('write') ? SharingPlugin::ACCESS_READWRITE : SharingPlugin::ACCESS_READ);
201199

202200
$entityManager = $doctrine->getManager();
203201

src/Controller/DAVController.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -248,6 +248,7 @@ private function initServer(string $authMethod, string $authRealm = User::DEFAUL
248248

249249
$aclPlugin = new PublicAwareDAVACLPlugin($this->em, $this->publicCalendarsEnabled);
250250
$aclPlugin->hideNodesFromListings = true;
251+
$aclPlugin->allowUnauthenticatedAccess = true; // Already the default, but setting it is future-proof
251252

252253
// Fetch admins, if any
253254
$admins = $this->em->getRepository(Principal::class)->findBy(['isAdmin' => true]);

src/Entity/CalendarInstance.php

Lines changed: 17 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use App\Constants;
66
use Doctrine\ORM\Mapping as ORM;
7+
use Sabre\DAV\Sharing\Plugin as SharingPlugin;
78
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
89
use Symfony\Component\Validator\Constraints as Assert;
910

@@ -12,27 +13,11 @@
1213
#[UniqueEntity(fields: ['principalUri', 'uri'], errorPath: 'uri', message: 'form.uri.unique')]
1314
class CalendarInstance
1415
{
15-
public const INVITE_NORESPONSE = 1;
16-
public const INVITE_ACCEPTED = 2;
17-
public const INVITE_DECLINED = 3;
18-
public const INVITE_INVALID = 4;
19-
20-
public const ACCESS_NOTSHARED = 0;
21-
public const ACCESS_SHAREDOWNER = 1;
22-
public const ACCESS_READ = 2;
23-
public const ACCESS_READWRITE = 3;
24-
public const ACCESS_NOACCESS = 4;
25-
26-
// Used to identify a public calendar, available to anyone without logging in.
27-
// It can't be shared, and it's owned by the principal.
28-
public const ACCESS_PUBLIC = 10;
29-
3016
public static function getOwnerAccesses(): array
3117
{
3218
return [
33-
self::ACCESS_NOTSHARED,
34-
self::ACCESS_SHAREDOWNER,
35-
self::ACCESS_PUBLIC,
19+
SharingPlugin::ACCESS_NOTSHARED,
20+
SharingPlugin::ACCESS_SHAREDOWNER,
3621
];
3722
}
3823

@@ -83,12 +68,16 @@ public static function getOwnerAccesses(): array
8368
#[ORM\Column(name: 'share_invitestatus', type: 'integer', options: ['default' => 2])]
8469
private $shareInviteStatus;
8570

71+
#[ORM\Column(name: 'public', type: 'boolean', options: ['default' => false])]
72+
private $public;
73+
8674
public function __construct()
8775
{
88-
$this->shareInviteStatus = self::INVITE_ACCEPTED;
76+
$this->shareInviteStatus = SharingPlugin::INVITE_ACCEPTED;
8977
$this->transparent = 0;
9078
$this->calendarOrder = 0;
91-
$this->access = self::ACCESS_SHAREDOWNER;
79+
$this->access = SharingPlugin::ACCESS_SHAREDOWNER;
80+
$this->public = false;
9281
}
9382

9483
public function getId(): ?int
@@ -137,9 +126,16 @@ public function isShared(): bool
137126
return !in_array($this->access, self::getOwnerAccesses());
138127
}
139128

129+
public function setPublic(bool $public): self
130+
{
131+
$this->public = $public;
132+
133+
return $this;
134+
}
135+
140136
public function isPublic(): bool
141137
{
142-
return self::ACCESS_PUBLIC === $this->access;
138+
return $this->public;
143139
}
144140

145141
public function isAutomaticallyGenerated(): bool

src/Form/CalendarInstanceType.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,6 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
2929
])
3030
->add('public', ChoiceType::class, [
3131
'label' => 'form.public',
32-
'mapped' => false,
3332
'disabled' => $options['shared'],
3433
'help' => 'form.public.help.caldav',
3534
'required' => true,

src/Plugins/PublicAwareDAVACLPlugin.php

Lines changed: 21 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -41,33 +41,31 @@ public function beforeMethod(RequestInterface $request, ResponseInterface $respo
4141

4242
public function getAcl($node): array
4343
{
44+
// Note:
45+
// '{DAV:}unauthenticated' - only unauthenticated users
46+
// '{DAV:}all' - all users (both authenticated and unauthenticated)
47+
// '{DAV:}authenticated' - only authenticated users
4448
$acl = parent::getAcl($node);
4549

46-
if ($node instanceof \Sabre\CalDAV\Calendar) {
47-
if (CalendarInstance::ACCESS_PUBLIC === $node->getShareAccess() && $this->public_calendars_enabled) {
48-
// We must add the ACL on the calendar itself
49-
$acl[] = [
50-
'principal' => '{DAV:}unauthenticated',
51-
'privilege' => '{DAV:}read',
52-
'protected' => false,
53-
];
54-
}
55-
} elseif ($node instanceof \Sabre\CalDAV\CalendarObject) {
56-
// The property is private in \Sabre\CalDAV\CalendarObject and we don't want to create
57-
// a new class just to access it, so we use a closure.
58-
$calendarInfo = (fn () => $this->calendarInfo)->call($node);
59-
// [0] is the calendarId, [1] is the calendarInstanceId
60-
$calendarInstanceId = $calendarInfo['id'][1];
50+
if ($this->public_calendars_enabled) {
51+
// Handle both Calendar AND SharedCalendar (which extends Calendar)
52+
if ($node instanceof \Sabre\CalDAV\Calendar || $node instanceof \Sabre\CalDAV\CalendarObject) {
53+
// The property is private in \Sabre\CalDAV\CalendarObject and we don't want to create
54+
// a new class just to access it, so we use a closure.
55+
$calendarInfo = (fn () => $this->calendarInfo)->call($node);
56+
// [0] is the calendarId, [1] is the calendarInstanceId
57+
$calendarInstanceId = $calendarInfo['id'][1];
6158

62-
$calendar = $this->em->getRepository(CalendarInstance::class)->findOneById($calendarInstanceId);
59+
$calendar = $this->em->getRepository(CalendarInstance::class)->findOneById($calendarInstanceId);
6360

64-
if ($calendar && $calendar->isPublic() && $this->public_calendars_enabled) {
65-
// We must add the ACL on the object itself
66-
$acl[] = [
67-
'principal' => '{DAV:}unauthenticated',
68-
'privilege' => '{DAV:}read',
69-
'protected' => false,
70-
];
61+
if ($calendar && $calendar->isPublic()) {
62+
// Add unauthenticated read access on the object itself
63+
$acl[] = [
64+
'principal' => '{DAV:}unauthenticated',
65+
'privilege' => '{DAV:}read',
66+
'protected' => false,
67+
];
68+
}
7169
}
7270
}
7371

src/Services/BirthdayService.php

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@
1919
use App\Entity\Card;
2020
use App\Entity\Principal;
2121
use Doctrine\Persistence\ManagerRegistry;
22+
use Sabre\DAV\Sharing\Plugin as SharingPlugin;
2223
use Sabre\VObject\Component\VCalendar;
2324
use Sabre\VObject\Component\VCard;
2425
use Sabre\VObject\DateTimeParser;
@@ -94,7 +95,7 @@ public function ensureBirthdayCalendarExists(string $principalUri): CalendarInst
9495
->setPrincipalUri($principalUri)
9596
->setDisplayName('🎁 Birthdays')
9697
->setDescription('Birthdays')
97-
->setAccess(CalendarInstance::ACCESS_READ)
98+
->setAccess(SharingPlugin::ACCESS_READ)
9899
->setCalendarOrder(0)
99100
->setCalendar($calendar)
100101
->setTransparent(1)

templates/calendars/index.html.twig

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,8 @@
1515
<div class="d-flex w-100 justify-content-between">
1616
<h5 class="mb-1 me-auto">
1717
{{ calendar.displayName }}
18-
{% if calendar.access == constant('\\App\\Entity\\CalendarInstance::ACCESS_PUBLIC') %}
19-
<span class="badge bg-success ml-1">{{ ('calendar.share_access.' ~ calendar.access)|trans }}</span>
18+
{% if calendar.isPublic() %}
19+
<span class="badge bg-success ml-1">{{ ('calendar.public')|trans }}</span>
2020
{% endif %}
2121
<a href="#" tabindex="0" class="badge badge-indicator" role="button" data-bs-toggle="popover" data-bs-title="{{ 'calendars.setup.title'|trans }}" data-bs-html='true' data-bs-content="URI: <code>{{ calendar.uri }}</code><br />Absolute path: <code>{{ davUri }}</code>">ⓘ</a>
2222
<span class="badge badge-indicator" style="background-color: {{ calendar.calendarColor }}">&nbsp;</span>
@@ -79,7 +79,7 @@
7979
<div class="d-flex w-100 justify-content-between">
8080
<h5 class="mb-1 me-auto">
8181
{{ calendar.displayName }}
82-
{% if calendar.access == constant('\\App\\Entity\\CalendarInstance::ACCESS_READWRITE') %}
82+
{% if calendar.access == constant('Sabre\\DAV\\Sharing\\Plugin::ACCESS_READWRITE') %}
8383
<span class="badge bg-success ms-1">{{ ('calendar.share_access.' ~ calendar.access)|trans }}</span>
8484
{% else %}
8585
<span class="badge bg-info ms-1">{{ ('calendar.share_access.' ~ calendar.access)|trans }}</span>

translations/messages+intl-icu.de.xlf

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -585,8 +585,8 @@
585585
<source>calendar.share_access.3</source>
586586
<target>lesen / schreiben</target>
587587
</trans-unit>
588-
<trans-unit id="F8h2YaT" resname="calendar.share_access.10">
589-
<source>calendar.share_access.10</source>
588+
<trans-unit id="F8h2YaT" resname="calendar.public">
589+
<source>calendar.public</source>
590590
<target>öffentlich</target>
591591
</trans-unit>
592592
<trans-unit id="mYDsetM" resname="calendar.auto">

0 commit comments

Comments
 (0)