Skip to content

Commit 2989deb

Browse files
committed
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>
1 parent 65e9409 commit 2989deb

File tree

9 files changed

+315
-8
lines changed

9 files changed

+315
-8
lines changed

apps/dav/composer/composer/autoload_classmap.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@
3737
'OCA\\DAV\\CalDAV\\Activity\\Setting\\Calendar' => $baseDir . '/../lib/CalDAV/Activity/Setting/Calendar.php',
3838
'OCA\\DAV\\CalDAV\\Activity\\Setting\\Event' => $baseDir . '/../lib/CalDAV/Activity/Setting/Event.php',
3939
'OCA\\DAV\\CalDAV\\Activity\\Setting\\Todo' => $baseDir . '/../lib/CalDAV/Activity/Setting/Todo.php',
40+
'OCA\\DAV\\CalDAV\\AppCalendar\\AppCalendar' => $baseDir . '/../lib/CalDAV/AppCalendar/AppCalendar.php',
41+
'OCA\\DAV\\CalDAV\\AppCalendar\\AppCalendarPlugin' => $baseDir . '/../lib/CalDAV/AppCalendar/AppCalendarPlugin.php',
42+
'OCA\\DAV\\CalDAV\\AppCalendar\\CalendarObject' => $baseDir . '/../lib/CalDAV/AppCalendar/CalendarObject.php',
4043
'OCA\\DAV\\CalDAV\\Auth\\CustomPrincipalPlugin' => $baseDir . '/../lib/CalDAV/Auth/CustomPrincipalPlugin.php',
4144
'OCA\\DAV\\CalDAV\\Auth\\PublicPrincipalPlugin' => $baseDir . '/../lib/CalDAV/Auth/PublicPrincipalPlugin.php',
4245
'OCA\\DAV\\CalDAV\\BirthdayCalendar\\EnablePlugin' => $baseDir . '/../lib/CalDAV/BirthdayCalendar/EnablePlugin.php',

apps/dav/composer/composer/autoload_static.php

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,9 @@ class ComposerStaticInitDAV
5252
'OCA\\DAV\\CalDAV\\Activity\\Setting\\Calendar' => __DIR__ . '/..' . '/../lib/CalDAV/Activity/Setting/Calendar.php',
5353
'OCA\\DAV\\CalDAV\\Activity\\Setting\\Event' => __DIR__ . '/..' . '/../lib/CalDAV/Activity/Setting/Event.php',
5454
'OCA\\DAV\\CalDAV\\Activity\\Setting\\Todo' => __DIR__ . '/..' . '/../lib/CalDAV/Activity/Setting/Todo.php',
55+
'OCA\\DAV\\CalDAV\\AppCalendar\\AppCalendar' => __DIR__ . '/..' . '/../lib/CalDAV/AppCalendar/AppCalendar.php',
56+
'OCA\\DAV\\CalDAV\\AppCalendar\\AppCalendarPlugin' => __DIR__ . '/..' . '/../lib/CalDAV/AppCalendar/AppCalendarPlugin.php',
57+
'OCA\\DAV\\CalDAV\\AppCalendar\\CalendarObject' => __DIR__ . '/..' . '/../lib/CalDAV/AppCalendar/CalendarObject.php',
5558
'OCA\\DAV\\CalDAV\\Auth\\CustomPrincipalPlugin' => __DIR__ . '/..' . '/../lib/CalDAV/Auth/CustomPrincipalPlugin.php',
5659
'OCA\\DAV\\CalDAV\\Auth\\PublicPrincipalPlugin' => __DIR__ . '/..' . '/../lib/CalDAV/Auth/PublicPrincipalPlugin.php',
5760
'OCA\\DAV\\CalDAV\\BirthdayCalendar\\EnablePlugin' => __DIR__ . '/..' . '/../lib/CalDAV/BirthdayCalendar/EnablePlugin.php',

apps/dav/lib/AppInfo/Application.php

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@
3535
use Exception;
3636
use OCA\DAV\BackgroundJob\UpdateCalendarResourcesRoomsBackgroundJob;
3737
use OCA\DAV\CalDAV\Activity\Backend;
38+
use OCA\DAV\CalDAV\AppCalendar\AppCalendarPlugin;
3839
use OCA\DAV\CalDAV\CalendarManager;
3940
use OCA\DAV\CalDAV\CalendarProvider;
4041
use OCA\DAV\CalDAV\Reminder\NotificationProvider\AudioProvider;
@@ -44,7 +45,6 @@
4445
use OCA\DAV\CalDAV\Reminder\Notifier;
4546

4647
use OCA\DAV\Capabilities;
47-
use OCA\DAV\CardDAV\CardDavBackend;
4848
use OCA\DAV\CardDAV\ContactsManager;
4949
use OCA\DAV\CardDAV\PhotoCache;
5050
use OCA\DAV\CardDAV\SyncService;
@@ -100,6 +100,7 @@
100100
use OCP\Config\BeforePreferenceDeletedEvent;
101101
use OCP\Config\BeforePreferenceSetEvent;
102102
use OCP\Contacts\IManager as IContactsManager;
103+
use OCP\Files\AppData\IAppDataFactory;
103104
use OCP\IServerContainer;
104105
use OCP\IUser;
105106
use Psr\Container\ContainerInterface;
@@ -119,14 +120,17 @@ public function __construct() {
119120
public function register(IRegistrationContext $context): void {
120121
$context->registerServiceAlias('CardDAVSyncService', SyncService::class);
121122
$context->registerService(PhotoCache::class, function (ContainerInterface $c) {
122-
/** @var IServerContainer $server */
123-
$server = $c->get(IServerContainer::class);
124-
125123
return new PhotoCache(
126-
$server->getAppDataDir('dav-photocache'),
124+
$c->get(IAppDataFactory::class)->get('dav-photocache'),
127125
$c->get(LoggerInterface::class)
128126
);
129127
});
128+
$context->registerService(AppCalendarPlugin::class, function(ContainerInterface $c) {
129+
return new AppCalendarPlugin(
130+
$c->get(ICalendarManager::class),
131+
$c->get(LoggerInterface::class)
132+
);
133+
});
130134

131135
/*
132136
* Register capabilities

apps/dav/lib/AppInfo/PluginManager.php

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
namespace OCA\DAV\AppInfo;
3030

3131
use OC\ServerContainer;
32+
use OCA\DAV\CalDAV\AppCalendar\AppCalendarPlugin;
3233
use OCA\DAV\CalDAV\Integration\ICalendarProvider;
3334
use OCA\DAV\CardDAV\Integration\IAddressBookProvider;
3435
use OCP\App\IAppManager;
@@ -144,6 +145,8 @@ private function populate(): void {
144145
}
145146
$this->populated = true;
146147

148+
$this->calendarPlugins[] = $this->container->get(AppCalendarPlugin::class);
149+
147150
foreach ($this->appManager->getInstalledApps() as $app) {
148151
// load plugins and collections from info.xml
149152
$info = $this->appManager->getAppInfo($app);
@@ -253,7 +256,7 @@ private function extractCalendarPluginList(array $array): array {
253256

254257
private function createClass(string $className): object {
255258
try {
256-
return $this->container->query($className);
259+
return $this->container->get($className);
257260
} catch (QueryException $e) {
258261
if (class_exists($className)) {
259262
return new $className();
Lines changed: 157 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,157 @@
1+
<?php
2+
3+
namespace OCA\DAV\CalDAV\AppCalendar;
4+
5+
use OCA\DAV\CalDAV\Plugin;
6+
use OCA\DAV\CalDAV\Integration\ExternalCalendar;
7+
use OCP\Calendar\ICalendar;
8+
use OCP\Calendar\ICreateFromString;
9+
use OCP\Constants;
10+
use Sabre\CalDAV\CalendarQueryValidator;
11+
use Sabre\CalDAV\ICalendarObject;
12+
use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet;
13+
use Sabre\DAV\Exception\Forbidden;
14+
use Sabre\DAV\Exception\NotFound;
15+
use Sabre\DAV\PropPatch;
16+
use Sabre\VObject\Reader;
17+
18+
class AppCalendar extends ExternalCalendar {
19+
protected string $principal;
20+
protected ICalendar $calendar;
21+
22+
public function __construct(string $appId, ICalendar $calendar, string $principal) {
23+
parent::__construct($appId, $calendar->getUri());
24+
$this->principal = $principal;
25+
$this->calendar = $calendar;
26+
}
27+
28+
public function getOwner(): ?string {
29+
return $this->principal;
30+
}
31+
32+
public function getGroup(): ?string {
33+
return null;
34+
}
35+
36+
public function getACL(): array {
37+
$acl = [
38+
[
39+
'privilege' => '{DAV:}read',
40+
'principal' => $this->getOwner(),
41+
'protected' => true,
42+
],
43+
[
44+
'privilege' => '{DAV:}write-properties',
45+
'principal' => $this->getOwner(),
46+
'protected' => true,
47+
]
48+
];
49+
if ($this->calendar instanceof ICreateFromString &&
50+
$this->calendar->getPermissions() & Constants::PERMISSION_CREATE) {
51+
$acl[] = [
52+
'privilege' => '{DAV:}write',
53+
'principal' => $this->getOwner(),
54+
'protected' => true,
55+
];
56+
}
57+
return $acl;
58+
}
59+
60+
public function setACL(array $acl): void {
61+
throw new Forbidden('Setting ACL is not supported on this node');
62+
}
63+
64+
public function getSupportedPrivilegeSet(): ?array {
65+
// Use the default one
66+
return null;
67+
}
68+
69+
public function getLastModified(): ?int {
70+
// unknown
71+
return null;
72+
}
73+
74+
public function delete(): void {
75+
// No delete method in OCP\Calendar\ICalendar
76+
throw new Forbidden('Deleting an entry is not implemented');
77+
}
78+
79+
public function createFile($name, $data = null) {
80+
if ($this->calendar instanceof ICreateFromString) {
81+
if (is_resource($data)) $data = stream_get_contents($data);
82+
$this->calendar->createFromString($name, is_null($data) ? '' : $data);
83+
return null;
84+
} else {
85+
throw new Forbidden('Creating a new entry is not implemented');
86+
}
87+
}
88+
89+
public function getProperties($properties) {
90+
return [
91+
'{DAV:}displayname' => $this->calendar->getDisplayName() ?: $this->calendar->getKey(),
92+
'{http://apple.com/ns/ical/}calendar-color' => '#' . ($this->calendar->getDisplayColor() ?: '0082c9'),
93+
'{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VEVENT', 'VTODO']),
94+
];
95+
}
96+
97+
public function calendarQuery(array $filters) {
98+
$result = [];
99+
$objects = $this->getChildren();
100+
101+
foreach ($objects as $object) {
102+
if ($this->validateFilterForObject($object, $filters)) {
103+
$result[] = $object->getName();
104+
}
105+
}
106+
107+
return $result;
108+
}
109+
110+
protected function validateFilterForObject(ICalendarObject $object, array $filters): bool {
111+
/** @var \Sabre\VObject\Component\VCalendar */
112+
$vObject = Reader::read($object->get());
113+
114+
$validator = new CalendarQueryValidator();
115+
$result = $validator->validate($vObject, $filters);
116+
117+
// Destroy circular references so PHP will GC the object.
118+
$vObject->destroy();
119+
120+
return $result;
121+
}
122+
123+
public function childExists($name): bool {
124+
try {
125+
$this->getChild($name);
126+
return true;
127+
} catch (NotFound $error) {
128+
return false;
129+
}
130+
}
131+
132+
public function getChild($name) {
133+
$pos = strrpos($name, '.ics');
134+
$children = $this->calendar->search(substr($name, 0, $pos === FALSE ? null : $pos), ['UID'], [], 1);
135+
136+
if (count($children) > 0) {
137+
return new CalendarObject($this, $children[0]);
138+
}
139+
140+
throw new NotFound('Node not found');
141+
}
142+
143+
/**
144+
* @return ICalendarObject[]
145+
*/
146+
public function getChildren(): array {
147+
$children = array_map(function ($calendar) {
148+
return new CalendarObject($this, $calendar);
149+
}, $this->calendar->search(''));
150+
151+
return $children;
152+
}
153+
154+
public function propPatch(PropPatch $propPatch): void {
155+
// no setDisplayColor or setDisplayName in \OCP\Calendar\ICalendar
156+
}
157+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
<?php
2+
3+
namespace OCA\DAV\CalDAV\AppCalendar;
4+
5+
use OCA\DAV\CalDAV\Integration\ExternalCalendar;
6+
use OCA\DAV\CalDAV\Integration\ICalendarProvider;
7+
use OCP\Calendar\IManager;
8+
use Psr\Log\LoggerInterface;
9+
10+
/* Plugin for wrapping application generated calendars registered in nextcloud core (OCP\Calendar\ICalendarProvider) */
11+
class AppCalendarPlugin implements ICalendarProvider {
12+
protected IManager $manager;
13+
protected LoggerInterface $logger;
14+
15+
public function __construct(IManager $manager, LoggerInterface $logger) {
16+
$this->manager = $manager;
17+
$this->logger = $logger;
18+
}
19+
20+
public function getAppID(): string {
21+
return 'dav-wrapper';
22+
}
23+
24+
public function fetchAllForCalendarHome(string $principalUri): array {
25+
return array_map(function ($calendar) use (&$principalUri) {
26+
return new AppCalendar($this->getAppID(), $calendar, $principalUri);
27+
}, $this->getWrappedCalendars($principalUri));
28+
}
29+
30+
public function hasCalendarInCalendarHome(string $principalUri, string $calendarUri): bool {
31+
return count($this->getWrappedCalendars($principalUri, [ $calendarUri ])) > 0;
32+
}
33+
34+
public function getCalendarInCalendarHome(string $principalUri, string $calendarUri): ?ExternalCalendar {
35+
$calendars = $this->getWrappedCalendars($principalUri, [ $calendarUri ]);
36+
if (count($calendars) > 0) {
37+
return new AppCalendar($this->getAppID(), $calendars[0], $principalUri);
38+
}
39+
40+
return null;
41+
}
42+
43+
protected function getWrappedCalendars(string $principalUri, array $calendarUris = []): array {
44+
return array_values(
45+
array_filter($this->manager->getCalendarsForPrincipal($principalUri, $calendarUris), function ($c) {
46+
// We must not provide a wrapper for DAV calendars
47+
return ! ($c instanceof \OCA\DAV\CalDAV\CalendarImpl);
48+
})
49+
);
50+
}
51+
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
<?php
2+
3+
namespace OCA\DAV\CalDAV\AppCalendar;
4+
5+
use Sabre\CalDAV\ICalendarObject;
6+
use Sabre\DAV\Exception\Forbidden;
7+
use Sabre\DAVACL\IACL;
8+
use Sabre\VObject\Component\VCalendar;
9+
use Sabre\VObject\Property\ICalendar\DateTime;
10+
11+
class CalendarObject implements ICalendarObject, IACL {
12+
private VCalendar $vobject;
13+
private AppCalendar $calendar;
14+
15+
public function __construct(AppCalendar $calendar, array $sourceItem) {
16+
$this->calendar = $calendar;
17+
$this->vobject = new VCalendar($sourceItem);
18+
}
19+
20+
public function getOwner() {
21+
return $this->calendar->getOwner();
22+
}
23+
24+
public function getGroup() {
25+
return $this->calendar->getGroup();
26+
}
27+
28+
public function getACL(): array {
29+
return [
30+
[
31+
'privilege' => '{DAV:}read',
32+
'principal' => $this->getOwner(),
33+
'protected' => true,
34+
]
35+
];
36+
}
37+
38+
public function setACL(array $acl): void {
39+
throw new Forbidden('Setting ACL is not supported on this node');
40+
}
41+
42+
public function getSupportedPrivilegeSet(): ?array {
43+
return null;
44+
}
45+
46+
public function put($data): void {
47+
throw new Forbidden('This calendar-object is read-only');
48+
}
49+
50+
public function get(): string {
51+
return $this->vobject->serialize();
52+
}
53+
54+
public function getContentType(): string {
55+
return 'text/calendar; charset=utf-8';
56+
}
57+
58+
public function getETag(): ?string {
59+
return null;
60+
}
61+
62+
public function getSize() {
63+
return mb_strlen($this->vobject->serialize());
64+
}
65+
66+
public function delete(): void {
67+
throw new Forbidden('This calendar-object is read-only');
68+
}
69+
70+
public function getName(): string {
71+
return (string) $this->vobject->getBaseComponent()->UID;
72+
}
73+
74+
public function setName($name): void {
75+
throw new Forbidden('This calendar-object is read-only');
76+
}
77+
78+
public function getLastModified(): ?int {
79+
$base = $this->vobject->getBaseComponent();
80+
if ($base !== null && $this->vobject->getBaseComponent()->{'LAST-MODIFIED'}) {
81+
/** @var DateTime */
82+
$lastModified = $this->vobject->getBaseComponent()->{'LAST-MODIFIED'};
83+
return $lastModified->getDateTime()->getTimestamp();
84+
}
85+
return null;
86+
}
87+
}

apps/dav/lib/CalDAV/CalendarProvider.php

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
namespace OCA\DAV\CalDAV;
2727

2828
use OCP\Calendar\ICalendarProvider;
29-
use OCP\Calendar\ICreateFromString;
3029
use OCP\IConfig;
3130
use OCP\IL10N;
3231
use Psr\Log\LoggerInterface;

0 commit comments

Comments
 (0)