Skip to content
Merged
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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@ Davis
[![Build Status][ci_badge]][ci_link]
[![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)
[![Latest release][release_badge]][release_link]
[![License](https://img.shields.io/github/license/tchapi/davis)](https://github.com/tchapi/davis/blob/main/LICENSE)
![Platform](https://img.shields.io/badge/platform-amd64%20%7C%20arm64-blue?logo=docker)
![PHP Version](https://img.shields.io/badge/php-8.2%20%7C%208.3%20%7C%208.4-777BB4?logo=php&logoColor=white)
[![Sponsor me][sponsor_badge]][sponsor_link]

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_)
Expand Down
60 changes: 60 additions & 0 deletions migrations/Version20260131161930.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
<?php

declare(strict_types=1);

namespace DoctrineMigrations;

use Doctrine\DBAL\Schema\Schema;
use Doctrine\Migrations\AbstractMigration;

/**
* Add public flag on calendar instance.
*/
final class Version20260131161930 extends AbstractMigration
{
public function getDescription(): string
{
return 'Add public flag on CalendarInstance (replacing ACCESS_PUBLIC flag)';
}

public function up(Schema $schema): void
{
$engine = $this->connection->getDatabasePlatform()->getName();

if ('mysql' === $engine) {
$this->addSql('ALTER TABLE calendarinstances ADD public TINYINT(1) DEFAULT 0 NOT NULL');
} elseif ('postgresql' === $engine) {
$this->addSql('ALTER TABLE calendarinstances ADD public BOOLEAN DEFAULT FALSE NOT NULL');
} elseif ('sqlite' === $engine) {
$this->addSql('ALTER TABLE calendarinstances ADD public BOOLEAN DEFAULT 0 NOT NULL');
}

// Migrate ACCESS_PUBLIC (10) to ACCESS_SHAREDOWNER (1) + public = true
if ('postgresql' === $engine) {
$this->addSql('UPDATE calendarinstances SET public = TRUE, access = 1 WHERE access = 10');
} else {
// MySQL and SQLite accept 1/0 for booleans
$this->addSql('UPDATE calendarinstances SET public = 1, access = 1 WHERE access = 10');
}
}

public function down(Schema $schema): void
{
$engine = $this->connection->getDatabasePlatform()->getName();

// Revert public = true back to ACCESS_PUBLIC (10)
if ('postgresql' === $engine) {
$this->addSql('UPDATE calendarinstances SET access = 10 WHERE is_public = TRUE');
} else {
$this->addSql('UPDATE calendarinstances SET access = 10 WHERE is_public = 1');
}

if ('mysql' === $engine) {
$this->addSql('ALTER TABLE calendarinstances DROP public');
} elseif ('postgresql' === $engine) {
$this->addSql('ALTER TABLE calendarinstances DROP COLUMN public');
} elseif ('sqlite' === $engine) {
$this->addSql('ALTER TABLE calendarinstances DROP COLUMN public');
}
}
}
12 changes: 5 additions & 7 deletions src/Controller/Admin/CalendarController.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
use App\Entity\SchedulingObject;
use App\Form\CalendarInstanceType;
use Doctrine\Persistence\ManagerRegistry;
use Sabre\DAV\Sharing\Plugin as SharingPlugin;
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
use Symfony\Component\HttpFoundation\JsonResponse;
use Symfony\Component\HttpFoundation\Request;
Expand Down Expand Up @@ -100,9 +101,6 @@ public function calendarEdit(ManagerRegistry $doctrine, Request $request, string
$form->get('events')->setData(in_array(Calendar::COMPONENT_EVENTS, $components));
$form->get('todos')->setData(in_array(Calendar::COMPONENT_TODOS, $components));
$form->get('notes')->setData(in_array(Calendar::COMPONENT_NOTES, $components));
if ($arePublicCalendarsEnabled) {
$form->get('public')->setData($calendarInstance->isPublic());
}
$form->get('principalUri')->setData(Principal::PREFIX.$username);

$form->handleRequest($request);
Expand All @@ -123,9 +121,9 @@ public function calendarEdit(ManagerRegistry $doctrine, Request $request, string
$components[] = Calendar::COMPONENT_NOTES;
}
if ($arePublicCalendarsEnabled && true === $form->get('public')->getData()) {
$calendarInstance->setAccess(CalendarInstance::ACCESS_PUBLIC);
$calendarInstance->setPublic(true);
} else {
$calendarInstance->setAccess(CalendarInstance::ACCESS_SHAREDOWNER);
$calendarInstance->setPublic(false);
}

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

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

$entityManager = $doctrine->getManager();

Expand Down
1 change: 1 addition & 0 deletions src/Controller/DAVController.php
Original file line number Diff line number Diff line change
Expand Up @@ -248,6 +248,7 @@ private function initServer(string $authMethod, string $authRealm = User::DEFAUL

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

// Fetch admins, if any
$admins = $this->em->getRepository(Principal::class)->findBy(['isAdmin' => true]);
Expand Down
38 changes: 17 additions & 21 deletions src/Entity/CalendarInstance.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

use App\Constants;
use Doctrine\ORM\Mapping as ORM;
use Sabre\DAV\Sharing\Plugin as SharingPlugin;
use Symfony\Bridge\Doctrine\Validator\Constraints\UniqueEntity;
use Symfony\Component\Validator\Constraints as Assert;

Expand All @@ -12,27 +13,11 @@
#[UniqueEntity(fields: ['principalUri', 'uri'], errorPath: 'uri', message: 'form.uri.unique')]
class CalendarInstance
{
public const INVITE_NORESPONSE = 1;
public const INVITE_ACCEPTED = 2;
public const INVITE_DECLINED = 3;
public const INVITE_INVALID = 4;

public const ACCESS_NOTSHARED = 0;
public const ACCESS_SHAREDOWNER = 1;
public const ACCESS_READ = 2;
public const ACCESS_READWRITE = 3;
public const ACCESS_NOACCESS = 4;

// Used to identify a public calendar, available to anyone without logging in.
// It can't be shared, and it's owned by the principal.
public const ACCESS_PUBLIC = 10;

public static function getOwnerAccesses(): array
{
return [
self::ACCESS_NOTSHARED,
self::ACCESS_SHAREDOWNER,
self::ACCESS_PUBLIC,
SharingPlugin::ACCESS_NOTSHARED,
SharingPlugin::ACCESS_SHAREDOWNER,
];
}

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

#[ORM\Column(name: 'public', type: 'boolean', options: ['default' => false])]
private $public;

public function __construct()
{
$this->shareInviteStatus = self::INVITE_ACCEPTED;
$this->shareInviteStatus = SharingPlugin::INVITE_ACCEPTED;
$this->transparent = 0;
$this->calendarOrder = 0;
$this->access = self::ACCESS_SHAREDOWNER;
$this->access = SharingPlugin::ACCESS_SHAREDOWNER;
$this->public = false;
}

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

public function setPublic(bool $public): self
{
$this->public = $public;

return $this;
}

public function isPublic(): bool
{
return self::ACCESS_PUBLIC === $this->access;
return $this->public;
}

public function isAutomaticallyGenerated(): bool
Expand Down
1 change: 0 additions & 1 deletion src/Form/CalendarInstanceType.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ public function buildForm(FormBuilderInterface $builder, array $options): void
])
->add('public', ChoiceType::class, [
'label' => 'form.public',
'mapped' => false,
'disabled' => $options['shared'],
'help' => 'form.public.help.caldav',
'required' => true,
Expand Down
44 changes: 21 additions & 23 deletions src/Plugins/PublicAwareDAVACLPlugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -41,33 +41,31 @@ public function beforeMethod(RequestInterface $request, ResponseInterface $respo

public function getAcl($node): array
{
// Note:
// '{DAV:}unauthenticated' - only unauthenticated users
// '{DAV:}all' - all users (both authenticated and unauthenticated)
// '{DAV:}authenticated' - only authenticated users
$acl = parent::getAcl($node);

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

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

if ($calendar && $calendar->isPublic() && $this->public_calendars_enabled) {
// We must add the ACL on the object itself
$acl[] = [
'principal' => '{DAV:}unauthenticated',
'privilege' => '{DAV:}read',
'protected' => false,
];
if ($calendar && $calendar->isPublic()) {
// Add unauthenticated read access on the object itself
$acl[] = [
'principal' => '{DAV:}unauthenticated',
'privilege' => '{DAV:}read',
'protected' => false,
];
}
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/Services/BirthdayService.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
use App\Entity\Card;
use App\Entity\Principal;
use Doctrine\Persistence\ManagerRegistry;
use Sabre\DAV\Sharing\Plugin as SharingPlugin;
use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\Component\VCard;
use Sabre\VObject\DateTimeParser;
Expand Down Expand Up @@ -94,7 +95,7 @@ public function ensureBirthdayCalendarExists(string $principalUri): CalendarInst
->setPrincipalUri($principalUri)
->setDisplayName('🎁 Birthdays')
->setDescription('Birthdays')
->setAccess(CalendarInstance::ACCESS_READ)
->setAccess(SharingPlugin::ACCESS_READ)
->setCalendarOrder(0)
->setCalendar($calendar)
->setTransparent(1)
Expand Down
6 changes: 3 additions & 3 deletions templates/calendars/index.html.twig
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1 me-auto">
{{ calendar.displayName }}
{% if calendar.access == constant('\\App\\Entity\\CalendarInstance::ACCESS_PUBLIC') %}
<span class="badge bg-success ml-1">{{ ('calendar.share_access.' ~ calendar.access)|trans }}</span>
{% if calendar.isPublic() %}
<span class="badge bg-success ml-1">{{ ('calendar.public')|trans }}</span>
{% endif %}
<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>
<span class="badge badge-indicator" style="background-color: {{ calendar.calendarColor }}">&nbsp;</span>
Expand Down Expand Up @@ -79,7 +79,7 @@
<div class="d-flex w-100 justify-content-between">
<h5 class="mb-1 me-auto">
{{ calendar.displayName }}
{% if calendar.access == constant('\\App\\Entity\\CalendarInstance::ACCESS_READWRITE') %}
{% if calendar.access == constant('Sabre\\DAV\\Sharing\\Plugin::ACCESS_READWRITE') %}
<span class="badge bg-success ms-1">{{ ('calendar.share_access.' ~ calendar.access)|trans }}</span>
{% else %}
<span class="badge bg-info ms-1">{{ ('calendar.share_access.' ~ calendar.access)|trans }}</span>
Expand Down
4 changes: 2 additions & 2 deletions translations/messages+intl-icu.de.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -585,8 +585,8 @@
<source>calendar.share_access.3</source>
<target>lesen / schreiben</target>
</trans-unit>
<trans-unit id="F8h2YaT" resname="calendar.share_access.10">
<source>calendar.share_access.10</source>
<trans-unit id="F8h2YaT" resname="calendar.public">
<source>calendar.public</source>
<target>öffentlich</target>
</trans-unit>
<trans-unit id="mYDsetM" resname="calendar.auto">
Expand Down
4 changes: 2 additions & 2 deletions translations/messages+intl-icu.en.xlf
Original file line number Diff line number Diff line change
Expand Up @@ -585,8 +585,8 @@
<source>calendar.share_access.3</source>
<target>read / write</target>
</trans-unit>
<trans-unit id="F8h2YaT" resname="calendar.share_access.10">
<source>calendar.share_access.10</source>
<trans-unit id="F8h2YaT" resname="calendar.public">
<source>calendar.public</source>
<target>public</target>
</trans-unit>
<trans-unit id="mYDsetM" resname="calendar.auto">
Expand Down