-
-
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). Signed-off-by: Ferdinand Thiessen <rpm@fthiessen.de>
- Loading branch information
Showing
9 changed files
with
307 additions
and
10 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,153 @@ | ||
<?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\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; | ||
} | ||
|
||
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(): void { | ||
// 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) { | ||
if (is_resource($data)) $data = stream_get_contents($data); | ||
$this->calendar->createFromString($name, is_null($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(ICalendarObject $object, array $filters): bool { | ||
$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) { | ||
$pos = strrpos($name, '.ics'); | ||
$children = $this->calendar->search(substr($name, 0, $pos === FALSE ? null : $pos), ['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): 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); | ||
}) | ||
); | ||
} | ||
} |
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,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): void { | ||
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(): void { | ||
throw new Forbidden('This calendar-object is read-only'); | ||
} | ||
|
||
public function getName() { | ||
return (string) $this->vobject->getBaseComponent()->UID; | ||
} | ||
|
||
public function setName($name) { | ||
throw new Forbidden('This calendar-object is read-only'); | ||
} | ||
|
||
public function getLastModified() { | ||
if ($this->vobject->getBaseComponent()->{'LAST-MODIFIED'}) { | ||
return $this->vobject->getBaseComponent()->{'LAST-MODIFIED'}->getDateTime()->getTimestamp(); | ||
} | ||
return null; | ||
} | ||
} |
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
Oops, something went wrong.