-
-
Notifications
You must be signed in to change notification settings - Fork 4.1k
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Feature: Provide access to app generated calendars through CalDAV
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
Showing
12 changed files
with
689 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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); | ||
}) | ||
); | ||
} | ||
} |
Oops, something went wrong.