Skip to content

Commit

Permalink
Feature: Provide access to app generated calendars through CalDAV
Browse files Browse the repository at this point in the history
This adds CalDAV support for app generated calendars,
which are registered to the nextcloud core.
This is done by adding a dav plugin which wraps
all ICalendarProviders into a Sabre plugin (inspired by the deck app).

Signed-off-by: Ferdinand Thiessen <rpm@fthiessen.de>
  • Loading branch information
susnux committed Nov 12, 2022
1 parent 65e9409 commit e87fd70
Show file tree
Hide file tree
Showing 9 changed files with 306 additions and 10 deletions.
3 changes: 3 additions & 0 deletions apps/dav/composer/composer/autoload_classmap.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,9 @@
'OCA\\DAV\\CalDAV\\Activity\\Setting\\Calendar' => $baseDir . '/../lib/CalDAV/Activity/Setting/Calendar.php',
'OCA\\DAV\\CalDAV\\Activity\\Setting\\Event' => $baseDir . '/../lib/CalDAV/Activity/Setting/Event.php',
'OCA\\DAV\\CalDAV\\Activity\\Setting\\Todo' => $baseDir . '/../lib/CalDAV/Activity/Setting/Todo.php',
'OCA\\DAV\\CalDAV\\AppCalendar\\AppCalendar' => $baseDir . '/../lib/CalDAV/AppCalendar/AppCalendar.php',
'OCA\\DAV\\CalDAV\\AppCalendar\\AppCalendarPlugin' => $baseDir . '/../lib/CalDAV/AppCalendar/AppCalendarPlugin.php',
'OCA\\DAV\\CalDAV\\AppCalendar\\CalendarObject' => $baseDir . '/../lib/CalDAV/AppCalendar/CalendarObject.php',
'OCA\\DAV\\CalDAV\\Auth\\CustomPrincipalPlugin' => $baseDir . '/../lib/CalDAV/Auth/CustomPrincipalPlugin.php',
'OCA\\DAV\\CalDAV\\Auth\\PublicPrincipalPlugin' => $baseDir . '/../lib/CalDAV/Auth/PublicPrincipalPlugin.php',
'OCA\\DAV\\CalDAV\\BirthdayCalendar\\EnablePlugin' => $baseDir . '/../lib/CalDAV/BirthdayCalendar/EnablePlugin.php',
Expand Down
3 changes: 3 additions & 0 deletions apps/dav/composer/composer/autoload_static.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,9 @@ class ComposerStaticInitDAV
'OCA\\DAV\\CalDAV\\Activity\\Setting\\Calendar' => __DIR__ . '/..' . '/../lib/CalDAV/Activity/Setting/Calendar.php',
'OCA\\DAV\\CalDAV\\Activity\\Setting\\Event' => __DIR__ . '/..' . '/../lib/CalDAV/Activity/Setting/Event.php',
'OCA\\DAV\\CalDAV\\Activity\\Setting\\Todo' => __DIR__ . '/..' . '/../lib/CalDAV/Activity/Setting/Todo.php',
'OCA\\DAV\\CalDAV\\AppCalendar\\AppCalendar' => __DIR__ . '/..' . '/../lib/CalDAV/AppCalendar/AppCalendar.php',
'OCA\\DAV\\CalDAV\\AppCalendar\\AppCalendarPlugin' => __DIR__ . '/..' . '/../lib/CalDAV/AppCalendar/AppCalendarPlugin.php',
'OCA\\DAV\\CalDAV\\AppCalendar\\CalendarObject' => __DIR__ . '/..' . '/../lib/CalDAV/AppCalendar/CalendarObject.php',
'OCA\\DAV\\CalDAV\\Auth\\CustomPrincipalPlugin' => __DIR__ . '/..' . '/../lib/CalDAV/Auth/CustomPrincipalPlugin.php',
'OCA\\DAV\\CalDAV\\Auth\\PublicPrincipalPlugin' => __DIR__ . '/..' . '/../lib/CalDAV/Auth/PublicPrincipalPlugin.php',
'OCA\\DAV\\CalDAV\\BirthdayCalendar\\EnablePlugin' => __DIR__ . '/..' . '/../lib/CalDAV/BirthdayCalendar/EnablePlugin.php',
Expand Down
14 changes: 9 additions & 5 deletions apps/dav/lib/AppInfo/Application.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
use Exception;
use OCA\DAV\BackgroundJob\UpdateCalendarResourcesRoomsBackgroundJob;
use OCA\DAV\CalDAV\Activity\Backend;
use OCA\DAV\CalDAV\AppCalendar\AppCalendarPlugin;
use OCA\DAV\CalDAV\CalendarManager;
use OCA\DAV\CalDAV\CalendarProvider;
use OCA\DAV\CalDAV\Reminder\NotificationProvider\AudioProvider;
Expand All @@ -44,7 +45,6 @@
use OCA\DAV\CalDAV\Reminder\Notifier;

use OCA\DAV\Capabilities;
use OCA\DAV\CardDAV\CardDavBackend;
use OCA\DAV\CardDAV\ContactsManager;
use OCA\DAV\CardDAV\PhotoCache;
use OCA\DAV\CardDAV\SyncService;
Expand Down Expand Up @@ -100,6 +100,7 @@
use OCP\Config\BeforePreferenceDeletedEvent;
use OCP\Config\BeforePreferenceSetEvent;
use OCP\Contacts\IManager as IContactsManager;
use OCP\Files\AppData\IAppDataFactory;
use OCP\IServerContainer;
use OCP\IUser;
use Psr\Container\ContainerInterface;
Expand All @@ -119,14 +120,17 @@ public function __construct() {
public function register(IRegistrationContext $context): void {
$context->registerServiceAlias('CardDAVSyncService', SyncService::class);
$context->registerService(PhotoCache::class, function (ContainerInterface $c) {
/** @var IServerContainer $server */
$server = $c->get(IServerContainer::class);

return new PhotoCache(
$server->getAppDataDir('dav-photocache'),
$c->get(IAppDataFactory::class)->get('dav-photocache'),
$c->get(LoggerInterface::class)
);
});
$context->registerService(AppCalendarPlugin::class, function(ContainerInterface $c) {
return new AppCalendarPlugin(
$c->get(ICalendarManager::class),
$c->get(LoggerInterface::class)
);
});

/*
* Register capabilities
Expand Down
9 changes: 6 additions & 3 deletions apps/dav/lib/AppInfo/PluginManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,11 @@
namespace OCA\DAV\AppInfo;

use OC\ServerContainer;
use OCA\DAV\CalDAV\AppCalendar\AppCalendarPlugin;
use OCA\DAV\CalDAV\Integration\ICalendarProvider;
use OCA\DAV\CardDAV\Integration\IAddressBookProvider;
use OCP\App\IAppManager;
use OCP\AppFramework\QueryException;
use Psr\Container\ContainerExceptionInterface;
use Sabre\DAV\Collection;
use Sabre\DAV\ServerPlugin;
use function array_map;
Expand Down Expand Up @@ -144,6 +145,8 @@ private function populate(): void {
}
$this->populated = true;

$this->calendarPlugins[] = $this->container->get(AppCalendarPlugin::class);

foreach ($this->appManager->getInstalledApps() as $app) {
// load plugins and collections from info.xml
$info = $this->appManager->getAppInfo($app);
Expand Down Expand Up @@ -253,8 +256,8 @@ private function extractCalendarPluginList(array $array): array {

private function createClass(string $className): object {
try {
return $this->container->query($className);
} catch (QueryException $e) {
return $this->container->get($className);
} catch (ContainerExceptionInterface $e) {
if (class_exists($className)) {
return new $className();
}
Expand Down
152 changes: 152 additions & 0 deletions apps/dav/lib/CalDAV/AppCalendar/AppCalendar.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
<?php

namespace OCA\DAV\CalDAV\AppCalendar;

use OCA\DAV\CalDAV\Plugin;
use OCA\DAV\CalDAV\Integration\ExternalCalendar;
use OCP\Calendar\ICalendar;
use OCP\Calendar\ICreateFromString;
use OCP\Calendar\IManager;
use OCP\Constants;
use Sabre\CalDAV\CalendarQueryValidator;
use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\PropPatch;
use Sabre\VObject\Reader;

class AppCalendar extends ExternalCalendar {
protected string $principal;
protected IManager $manager;
protected ICalendar $calendar;

public function __construct(string $appId, ICalendar $calendar, string $principal) {
parent::__construct($appId, $calendar->getUri());
$this->principal = $principal;
$this->calendar = $calendar;
}

public function getOwner(): ?string {
return $this->principal;
}

public function getGroup(): ?string {
return null;
}

public function getACL(): array {
$acl = [
[
'privilege' => '{DAV:}read',
'principal' => $this->getOwner(),
'protected' => true,
],
[
'privilege' => '{DAV:}write-properties',
'principal' => $this->getOwner(),
'protected' => true,
]
];
if ($this->calendar instanceof ICreateFromString &&
$this->calendar->getPermissions() & Constants::PERMISSION_CREATE) {
$acl[] = [
'privilege' => '{DAV:}write',
'principal' => $this->getOwner(),
'protected' => true,
];
}
return $acl;
}

public function setACL(array $acl): void {
throw new Forbidden('Setting ACL is not supported on this node');
}

public function getSupportedPrivilegeSet(): ?array {
// Use the default one
return null;
}

public function getLastModified(): ?int {
// unknown
return null;
}

public function delete() {
// No delete method in OCP\Calendar\ICalendar
throw new Forbidden('Deleting an entry is not implemented');
}

public function createFile($name, $data = null) {
if ($this->calendar instanceof ICreateFromString) {
$this->calendar->createFromString($name, is_resource($data) ? stream_get_contents($data) : $data);
return null;
} else {
throw new Forbidden('Creating a new entry is not implemented');
}
}

public function getProperties($properties) {
return [
'{DAV:}displayname' => $this->calendar->getDisplayName() ?: $this->calendar->getKey(),
'{http://apple.com/ns/ical/}calendar-color' => '#' . ($this->calendar->getDisplayColor() ?: '0082c9'),
'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VEVENT', 'VTODO']),
];
}

public function calendarQuery(array $filters) {
$result = [];
$objects = $this->getChildren();

foreach ($objects as $object) {
if ($this->validateFilterForObject($object, $filters)) {
$result[] = $object->getName();
}
}

return $result;
}

protected function validateFilterForObject($object, array $filters) {
$vObject = Reader::read($object->get());

$validator = new CalendarQueryValidator();
$result = $validator->validate($vObject, $filters);

// Destroy circular references so PHP will GC the object.
$vObject->destroy();

return $result;
}

public function childExists($name): bool {
try {
$this->getChild($name);
return true;
} catch (NotFound $error) {
return false;
}
}

public function getChild($name) {
$children = $this->calendar->search(substr($name, 0, strrpos($name, '.ics')), ['UID'], [], 1);

if (count($children) > 0) {
return new CalendarObject($this, $children[0]);
}

throw new NotFound('Node not found');
}

public function getChildren() {
$children = array_map(function ($calendar) {
return new CalendarObject($this, $calendar);
}, $this->calendar->search(''));

return $children;
}

public function propPatch(PropPatch $propPatch) {
// no setDisplayColor or setDisplayName in \OCP\Calendar\ICalendar
}
}
51 changes: 51 additions & 0 deletions apps/dav/lib/CalDAV/AppCalendar/AppCalendarPlugin.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
<?php

namespace OCA\DAV\CalDAV\AppCalendar;

use OCA\DAV\CalDAV\Integration\ExternalCalendar;
use OCA\DAV\CalDAV\Integration\ICalendarProvider;
use OCP\Calendar\IManager;
use Psr\Log\LoggerInterface;

/* Plugin for wrapping application generated calendars registered in nextcloud core (OCP\Calendar\ICalendarProvider) */
class AppCalendarPlugin implements ICalendarProvider {
protected IManager $manager;
protected LoggerInterface $logger;

public function __construct(IManager $manager, LoggerInterface $logger) {
$this->manager = $manager;
$this->logger = $logger;
}

public function getAppID(): string {
return 'dav-wrapper';
}

public function fetchAllForCalendarHome(string $principalUri): array {
return array_map(function ($calendar) use (&$principalUri) {
return new AppCalendar($this->getAppID(), $calendar, $principalUri);
}, $this->getWrappedCalendars($principalUri));
}

public function hasCalendarInCalendarHome(string $principalUri, string $calendarUri): bool {
return count($this->getWrappedCalendars($principalUri, [ $calendarUri ])) > 0;
}

public function getCalendarInCalendarHome(string $principalUri, string $calendarUri): ?ExternalCalendar {
$calendars = $this->getWrappedCalendars($principalUri, [ $calendarUri ]);
if (count($calendars) > 0) {
return new AppCalendar($this->getAppID(), $calendars[0], $principalUri);
}

return null;
}

protected function getWrappedCalendars(string $principalUri, array $calendarUris = []): array {
return array_values(
array_filter($this->manager->getCalendarsForPrincipal($principalUri, $calendarUris), function ($c) {
// We must not provide a wrapper for DAV calendars
return ! ($c instanceof \OCA\DAV\CalDAV\CalendarImpl);
})
);
}
}
81 changes: 81 additions & 0 deletions apps/dav/lib/CalDAV/AppCalendar/CalendarObject.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php

namespace OCA\DAV\CalDAV\AppCalendar;

use Sabre\CalDAV\ICalendarObject;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAVACL\IACL;
use Sabre\VObject\Component\VCalendar;

class CalendarObject implements ICalendarObject, IACL {
private VCalendar $vobject;
private AppCalendar $calendar;

public function __construct(AppCalendar $calendar, array $sourceItem) {
$this->calendar = $calendar;
$this->vobject = new VCalendar($sourceItem);
}

public function getOwner() {
return $this->calendar->getOwner();
}

public function getGroup() {
return $this->calendar->getGroup();
}

public function getACL() {
return [[
'privilege' => '{DAV:}read',
'principal' => $this->getOwner(),
'protected' => true,
]];
}

public function setACL(array $acl) {
throw new Forbidden('Setting ACL is not supported on this node');
}

public function getSupportedPrivilegeSet() {
return null;
}

public function put($data) {
throw new Forbidden('This calendar-object is read-only');
}

public function get() {
return $this->vobject->serialize();
}

public function getContentType() {
return 'text/calendar; charset=utf-8';
}

public function getETag() {
return null;
}

public function getSize() {
return mb_strlen($this->vobject->serialize());
}

public function delete() {
throw new Forbidden('This calendar-object is read-only');
}

public function getName() {
return $this->vobject['UID'] . '.ics';
}

public function setName($name) {
throw new Forbidden('This calendar-object is read-only');
}

public function getLastModified() {
if ($this->vobject['LAST-MODIFIED']) {
return $this->vobject['LAST-MODIFIED']->getDateTime()->getTimestamp();
}
return null;
}
}
1 change: 0 additions & 1 deletion apps/dav/lib/CalDAV/CalendarProvider.php
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,6 @@
namespace OCA\DAV\CalDAV;

use OCP\Calendar\ICalendarProvider;
use OCP\Calendar\ICreateFromString;
use OCP\IConfig;
use OCP\IL10N;
use Psr\Log\LoggerInterface;
Expand Down
Loading

0 comments on commit e87fd70

Please sign in to comment.