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).

Add unit test for AppCalendar wrapper plugin and calendar object implementation.

Signed-off-by: Ferdinand Thiessen <rpm@fthiessen.de>
  • Loading branch information
susnux committed Feb 23, 2023
1 parent 710c593 commit 05eb849
Show file tree
Hide file tree
Showing 12 changed files with 689 additions and 19 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
5 changes: 4 additions & 1 deletion apps/dav/lib/AppInfo/PluginManager.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@
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;
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,7 +256,7 @@ private function extractCalendarPluginList(array $array): array {

private function createClass(string $className): object {
try {
return $this->container->query($className);
return $this->container->get($className);
} catch (QueryException $e) {
if (class_exists($className)) {
return new $className();
Expand Down
185 changes: 185 additions & 0 deletions apps/dav/lib/CalDAV/AppCalendar/AppCalendar.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
<?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\Constants;
use Sabre\CalDAV\CalendarQueryValidator;
use Sabre\CalDAV\ICalendarObject;
use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet;
use Sabre\DAV\Exception\Forbidden;
use Sabre\DAV\Exception\NotFound;
use Sabre\DAV\PropPatch;
use Sabre\VObject\Component\VCalendar;
use Sabre\VObject\Reader;

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

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

/**
* Return permissions supported by the backend calendar
* @return int Permissions based on \OCP\Constants
*/
public function getPermissions(): int {
// Make sure to only promote write support if the backend implement the correct interface
if ($this->calendar instanceof ICreateFromString) {
return $this->calendar->getPermissions();
}
return Constants::PERMISSION_READ;
}

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->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(): void {
// No method for deleting a calendar in OCP\Calendar\ICalendar
throw new Forbidden('Deleting an entry is not implemented');
}

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

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', 'VJOURNAL', '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(ICalendarObject $object, array $filters): bool {
/** @var \Sabre\VObject\Component\VCalendar */
$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) {
// Try to get calendar by filename
$children = $this->calendar->search($name, ['X-FILENAME']);
if (count($children) === 0) {
// If nothing found try to get by UID from filename
$pos = strrpos($name, '.ics');
$children = $this->calendar->search(substr($name, 0, $pos ?: null), ['UID']);
}

if (count($children) > 0) {
return new CalendarObject($this, $this->calendar, new VCalendar($children));
}

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

/**
* @return ICalendarObject[]
*/
public function getChildren(): array {
$objects = $this->calendar->search('');
// We need to group by UID (actually by filename but we do not have that information)
$result = [];
foreach ($objects as $object) {
$uid = (string)$object['UID'] ?: uniqid();
if (!isset($result[$uid])) {
$result[$uid] = [];
}
$result[$uid][] = $object;
}

return array_map(function (array $children) {
return new CalendarObject($this, $this->calendar, new VCalendar($children));
}, $result);
}

public function propPatch(PropPatch $propPatch): void {
// 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);
})
);
}
}
Loading

0 comments on commit 05eb849

Please sign in to comment.