From 62c4ae78df74daa81f44f13bfc2e08c6c784956f Mon Sep 17 00:00:00 2001 From: Ferdinand Thiessen Date: Sat, 12 Nov 2022 17:42:12 +0100 Subject: [PATCH] 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 --- .../composer/composer/autoload_classmap.php | 3 + .../dav/composer/composer/autoload_static.php | 3 + apps/dav/lib/AppInfo/Application.php | 14 +- apps/dav/lib/AppInfo/PluginManager.php | 5 +- .../lib/CalDAV/AppCalendar/AppCalendar.php | 208 ++++++++++++++++++ .../CalDAV/AppCalendar/AppCalendarPlugin.php | 74 +++++++ .../lib/CalDAV/AppCalendar/CalendarObject.php | 153 +++++++++++++ apps/dav/lib/CalDAV/CalendarProvider.php | 1 - .../tests/unit/AppInfo/PluginManagerTest.php | 25 ++- .../CalDAV/AppCalendar/AppCalendarTest.php | 123 +++++++++++ .../CalDAV/AppCalendar/CalendarObjectTest.php | 166 ++++++++++++++ .../tests/unit/Command/DeleteCalendarTest.php | 2 +- 12 files changed, 758 insertions(+), 19 deletions(-) create mode 100644 apps/dav/lib/CalDAV/AppCalendar/AppCalendar.php create mode 100644 apps/dav/lib/CalDAV/AppCalendar/AppCalendarPlugin.php create mode 100644 apps/dav/lib/CalDAV/AppCalendar/CalendarObject.php create mode 100644 apps/dav/tests/unit/CalDAV/AppCalendar/AppCalendarTest.php create mode 100644 apps/dav/tests/unit/CalDAV/AppCalendar/CalendarObjectTest.php diff --git a/apps/dav/composer/composer/autoload_classmap.php b/apps/dav/composer/composer/autoload_classmap.php index a9bf60698fdb8..6745ffe41b451 100644 --- a/apps/dav/composer/composer/autoload_classmap.php +++ b/apps/dav/composer/composer/autoload_classmap.php @@ -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', diff --git a/apps/dav/composer/composer/autoload_static.php b/apps/dav/composer/composer/autoload_static.php index 48104281cd4b2..302a424d08ea6 100644 --- a/apps/dav/composer/composer/autoload_static.php +++ b/apps/dav/composer/composer/autoload_static.php @@ -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', diff --git a/apps/dav/lib/AppInfo/Application.php b/apps/dav/lib/AppInfo/Application.php index 8674986262623..10e1130f90783 100644 --- a/apps/dav/lib/AppInfo/Application.php +++ b/apps/dav/lib/AppInfo/Application.php @@ -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; @@ -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; @@ -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; @@ -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 diff --git a/apps/dav/lib/AppInfo/PluginManager.php b/apps/dav/lib/AppInfo/PluginManager.php index 0b83d6a9205a1..828818455f75b 100644 --- a/apps/dav/lib/AppInfo/PluginManager.php +++ b/apps/dav/lib/AppInfo/PluginManager.php @@ -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; @@ -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); @@ -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(); diff --git a/apps/dav/lib/CalDAV/AppCalendar/AppCalendar.php b/apps/dav/lib/CalDAV/AppCalendar/AppCalendar.php new file mode 100644 index 0000000000000..d67f1f5a81606 --- /dev/null +++ b/apps/dav/lib/CalDAV/AppCalendar/AppCalendar.php @@ -0,0 +1,208 @@ + + * + * @author Ferdinand Thiessen + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +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 + } +} diff --git a/apps/dav/lib/CalDAV/AppCalendar/AppCalendarPlugin.php b/apps/dav/lib/CalDAV/AppCalendar/AppCalendarPlugin.php new file mode 100644 index 0000000000000..cdf7cb9059a60 --- /dev/null +++ b/apps/dav/lib/CalDAV/AppCalendar/AppCalendarPlugin.php @@ -0,0 +1,74 @@ + + * + * @author Ferdinand Thiessen + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +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); + }) + ); + } +} diff --git a/apps/dav/lib/CalDAV/AppCalendar/CalendarObject.php b/apps/dav/lib/CalDAV/AppCalendar/CalendarObject.php new file mode 100644 index 0000000000000..985b137c9557e --- /dev/null +++ b/apps/dav/lib/CalDAV/AppCalendar/CalendarObject.php @@ -0,0 +1,153 @@ + + * + * @author Ferdinand Thiessen + * + * @license AGPL-3.0-or-later + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + * + */ + +namespace OCA\DAV\CalDAV\AppCalendar; + +use OCP\Calendar\ICalendar; +use OCP\Calendar\ICreateFromString; +use OCP\Constants; +use Sabre\CalDAV\ICalendarObject; +use Sabre\DAV\Exception\Forbidden; +use Sabre\DAV\Exception\NotFound; +use Sabre\DAVACL\IACL; +use Sabre\VObject\Component\VCalendar; +use Sabre\VObject\Property\ICalendar\DateTime; + +class CalendarObject implements ICalendarObject, IACL { + private VCalendar $vobject; + private AppCalendar $calendar; + private ICalendar|ICreateFromString $backend; + + public function __construct(AppCalendar $calendar, ICalendar $backend, VCalendar $vobject) { + $this->backend = $backend; + $this->calendar = $calendar; + $this->vobject = $vobject; + } + + public function getOwner() { + return $this->calendar->getOwner(); + } + + public function getGroup() { + return $this->calendar->getGroup(); + } + + public function getACL(): array { + $acl = [ + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->getOwner(), + 'protected' => true, + ] + ]; + if ($this->calendar->getPermissions() & Constants::PERMISSION_UPDATE) { + $acl[] = [ + 'privilege' => '{DAV:}write-content', + '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 { + return null; + } + + public function put($data): void { + if ($this->backend instanceof ICreateFromString && $this->calendar->getPermissions() & Constants::PERMISSION_UPDATE) { + if (is_resource($data)) { + $data = stream_get_contents($data) ?: ''; + } + $this->backend->createFromString($this->getName(), $data); + } else { + throw new Forbidden('This calendar-object is read-only'); + } + } + + public function get(): string { + return $this->vobject->serialize(); + } + + public function getContentType(): string { + return 'text/calendar; charset=utf-8'; + } + + public function getETag(): ?string { + return null; + } + + public function getSize() { + return mb_strlen($this->vobject->serialize()); + } + + public function delete(): void { + if ($this->backend instanceof ICreateFromString && $this->calendar->getPermissions() & Constants::PERMISSION_DELETE) { + /** @var \Sabre\VObject\Component[] */ + $components = $this->vobject->getBaseComponents(); + foreach ($components as $key => $component) { + $components[$key]->STATUS = 'CANCELLED'; + $components[$key]->SEQUENCE = isset($component->SEQUENCE) ? ((int)$component->SEQUENCE->getValue()) + 1 : 1; + if ($component->name === 'VEVENT') { + $components[$key]->METHOD = 'CANCEL'; + } + } + $this->backend->createFromString($this->getName(), (new VCalendar($components))->serialize()); + } else { + throw new Forbidden('This calendar-object is read-only'); + } + } + + public function getName(): string { + // Every object is required to have an UID + $base = $this->vobject->getBaseComponent(); + // This should never happen except the app provides invalid calendars (VEvent, VTodo... all require UID to be present) + if ($base === null) { + throw new NotFound('Invalid node'); + } + if (isset($base->{'X-FILENAME'})) { + return (string)$base->{'X-FILENAME'}; + } + return (string)$base->UID . '.ics'; + } + + public function setName($name): void { + throw new Forbidden('This calendar-object is read-only'); + } + + public function getLastModified(): ?int { + $base = $this->vobject->getBaseComponent(); + if ($base !== null && $this->vobject->getBaseComponent()->{'LAST-MODIFIED'}) { + /** @var DateTime */ + $lastModified = $this->vobject->getBaseComponent()->{'LAST-MODIFIED'}; + return $lastModified->getDateTime()->getTimestamp(); + } + return null; + } +} diff --git a/apps/dav/lib/CalDAV/CalendarProvider.php b/apps/dav/lib/CalDAV/CalendarProvider.php index 5779111add3ad..f29c601db2d27 100644 --- a/apps/dav/lib/CalDAV/CalendarProvider.php +++ b/apps/dav/lib/CalDAV/CalendarProvider.php @@ -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; diff --git a/apps/dav/tests/unit/AppInfo/PluginManagerTest.php b/apps/dav/tests/unit/AppInfo/PluginManagerTest.php index 17f8ffda62580..67dd047768540 100644 --- a/apps/dav/tests/unit/AppInfo/PluginManagerTest.php +++ b/apps/dav/tests/unit/AppInfo/PluginManagerTest.php @@ -29,6 +29,7 @@ use OC\App\AppManager; use OC\ServerContainer; use OCA\DAV\AppInfo\PluginManager; +use OCA\DAV\CalDAV\AppCalendar\AppCalendarPlugin; use OCA\DAV\CalDAV\Integration\ICalendarProvider; use Sabre\DAV\Collection; use Sabre\DAV\ServerPlugin; @@ -43,7 +44,6 @@ class PluginManagerTest extends TestCase { public function test(): void { $server = $this->createMock(ServerContainer::class); - $appManager = $this->createMock(AppManager::class); $appManager->method('getInstalledApps') ->willReturn(['adavapp', 'adavapp2']); @@ -94,6 +94,7 @@ public function test(): void { $pluginManager = new PluginManager($server, $appManager); + $appCalendarPlugin = $this->createMock(AppCalendarPlugin::class); $calendarPlugin1 = $this->createMock(ICalendarProvider::class); $calendarPlugin2 = $this->createMock(ICalendarProvider::class); $calendarPlugin3 = $this->createMock(ICalendarProvider::class); @@ -106,17 +107,18 @@ public function test(): void { $dummyCollection2 = $this->createMock(Collection::class); $dummy2Collection1 = $this->createMock(Collection::class); - $server->method('query') + $server->method('get') ->willReturnMap([ - ['\OCA\DAV\ADavApp\PluginOne', true, $dummyPlugin1], - ['\OCA\DAV\ADavApp\PluginTwo', true, $dummyPlugin2], - ['\OCA\DAV\ADavApp\CalendarPluginOne', true, $calendarPlugin1], - ['\OCA\DAV\ADavApp\CalendarPluginTwo', true, $calendarPlugin2], - ['\OCA\DAV\ADavApp\CollectionOne', true, $dummyCollection1], - ['\OCA\DAV\ADavApp\CollectionTwo', true, $dummyCollection2], - ['\OCA\DAV\ADavApp2\PluginOne', true, $dummy2Plugin1], - ['\OCA\DAV\ADavApp2\CalendarPluginOne', true, $calendarPlugin3], - ['\OCA\DAV\ADavApp2\CollectionOne', true, $dummy2Collection1], + [AppCalendarPlugin::class, $appCalendarPlugin], + ['\OCA\DAV\ADavApp\PluginOne', $dummyPlugin1], + ['\OCA\DAV\ADavApp\PluginTwo', $dummyPlugin2], + ['\OCA\DAV\ADavApp\CalendarPluginOne', $calendarPlugin1], + ['\OCA\DAV\ADavApp\CalendarPluginTwo', $calendarPlugin2], + ['\OCA\DAV\ADavApp\CollectionOne', $dummyCollection1], + ['\OCA\DAV\ADavApp\CollectionTwo', $dummyCollection2], + ['\OCA\DAV\ADavApp2\PluginOne', $dummy2Plugin1], + ['\OCA\DAV\ADavApp2\CalendarPluginOne', $calendarPlugin3], + ['\OCA\DAV\ADavApp2\CollectionOne', $dummy2Collection1], ]); $expectedPlugins = [ @@ -125,6 +127,7 @@ public function test(): void { $dummy2Plugin1, ]; $expectedCalendarPlugins = [ + $appCalendarPlugin, $calendarPlugin1, $calendarPlugin2, $calendarPlugin3, diff --git a/apps/dav/tests/unit/CalDAV/AppCalendar/AppCalendarTest.php b/apps/dav/tests/unit/CalDAV/AppCalendar/AppCalendarTest.php new file mode 100644 index 0000000000000..78ebf8b67a44b --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/AppCalendar/AppCalendarTest.php @@ -0,0 +1,123 @@ +. + * + */ +namespace OCA\DAV\Tests\unit\CalDAV\AppCalendar; + +use OCA\DAV\CalDAV\AppCalendar\AppCalendar; +use OCP\Calendar\ICalendar; +use OCP\Calendar\ICreateFromString; +use OCP\Constants; +use PHPUnit\Framework\MockObject\MockObject; +use Test\TestCase; + +use function Safe\rewind; + +class AppCalendarTest extends TestCase { + private $principal = 'principals/users/foo'; + + private AppCalendar $appCalendar; + private AppCalendar $writeableAppCalendar; + + private ICalendar|MockObject $calendar; + private ICalendar|MockObject $writeableCalendar; + + protected function setUp(): void { + parent::setUp(); + + $this->calendar = $this->getMockBuilder(ICalendar::class)->getMock(); + $this->calendar->method('getPermissions') + ->willReturn(Constants::PERMISSION_READ); + + $this->writeableCalendar = $this->getMockBuilder(ICreateFromString::class)->getMock(); + $this->writeableCalendar->method('getPermissions') + ->willReturn(Constants::PERMISSION_READ | Constants::PERMISSION_CREATE); + + $this->appCalendar = new AppCalendar('dav-wrapper', $this->calendar, $this->principal); + $this->writeableAppCalendar = new AppCalendar('dav-wrapper', $this->writeableCalendar, $this->principal); + } + + public function testGetPrincipal():void { + // Check that the correct name is returned + $this->assertEquals($this->principal, $this->appCalendar->getOwner()); + $this->assertEquals($this->principal, $this->writeableAppCalendar->getOwner()); + } + + public function testDelete(): void { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + $this->expectExceptionMessage('Deleting an entry is not implemented'); + + $this->appCalendar->delete(); + } + + public function testCreateFile() { + $this->writeableCalendar->expects($this->exactly(3)) + ->method('createFromString') + ->withConsecutive(['some-name', 'data'], ['other-name', ''], ['name', 'some data']); + + // pass data + $this->assertNull($this->writeableAppCalendar->createFile('some-name', 'data')); + // null is empty string + $this->assertNull($this->writeableAppCalendar->createFile('other-name', null)); + // resource to data + $fp = fopen('php://memory', 'r+'); + fwrite($fp, 'some data'); + rewind($fp); + $this->assertNull($this->writeableAppCalendar->createFile('name', $fp)); + fclose($fp); + } + + public function testCreateFile_readOnly() { + // If writing is not supported + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + $this->expectExceptionMessage('Creating a new entry is not allowed'); + + $this->appCalendar->createFile('some-name', 'data'); + } + + public function testSetACL(): void { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + $this->expectExceptionMessage('Setting ACL is not supported on this node'); + + $this->appCalendar->setACL([]); + } + + public function testGetACL():void { + $expectedRO = [ + [ + 'privilege' => '{DAV:}read', + 'principal' => $this->principal, + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}write-properties', + 'principal' => $this->principal, + 'protected' => true, + ] + ]; + $expectedRW = $expectedRO; + $expectedRW[] = [ + 'privilege' => '{DAV:}write', + 'principal' => $this->principal, + 'protected' => true, + ]; + + // Check that the correct ACL is returned (default be only readable) + $this->assertEquals($expectedRO, $this->appCalendar->getACL()); + $this->assertEquals($expectedRW, $this->writeableAppCalendar->getACL()); + } +} diff --git a/apps/dav/tests/unit/CalDAV/AppCalendar/CalendarObjectTest.php b/apps/dav/tests/unit/CalDAV/AppCalendar/CalendarObjectTest.php new file mode 100644 index 0000000000000..e7bd2cc0b95b7 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/AppCalendar/CalendarObjectTest.php @@ -0,0 +1,166 @@ +calendar = $this->createMock(AppCalendar::class); + $this->calendar->method('getOwner')->willReturn('owner'); + $this->calendar->method('getGroup')->willReturn('group'); + + $this->backend = $this->createMock(ICalendar::class); + $this->vobject = $this->createMock(VCalendar::class); + $this->calendarObject = new CalendarObject($this->calendar, $this->backend, $this->vobject); + } + + public function testGetOwner() { + $this->assertEquals($this->calendarObject->getOwner(), 'owner'); + } + + public function testGetGroup() { + $this->assertEquals($this->calendarObject->getGroup(), 'group'); + } + + public function testGetACL() { + $this->calendar->expects($this->exactly(2)) + ->method('getPermissions') + ->willReturnOnConsecutiveCalls(Constants::PERMISSION_READ, Constants::PERMISSION_ALL); + + // read only + $this->assertEquals($this->calendarObject->getACL(), [ + [ + 'privilege' => '{DAV:}read', + 'principal' => 'owner', + 'protected' => true, + ] + ]); + + // write permissions + $this->assertEquals($this->calendarObject->getACL(), [ + [ + 'privilege' => '{DAV:}read', + 'principal' => 'owner', + 'protected' => true, + ], + [ + 'privilege' => '{DAV:}write-content', + 'principal' => 'owner', + 'protected' => true, + ] + ]); + } + + public function testSetACL() { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + $this->calendarObject->setACL([]); + } + + public function testPut_readOnlyBackend() { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + $this->calendarObject->put('foo'); + } + + public function testPut_noPermissions() { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + + $backend = $this->createMock(ICreateFromString::class); + $calendarObject = new CalendarObject($this->calendar, $backend, $this->vobject); + + $this->calendar->expects($this->once()) + ->method('getPermissions') + ->willReturn(Constants::PERMISSION_READ); + + $calendarObject->put('foo'); + } + + public function testPut() { + $backend = $this->createMock(ICreateFromString::class); + $calendarObject = new CalendarObject($this->calendar, $backend, $this->vobject); + + $this->vobject->expects($this->once()) + ->method('getBaseComponent') + ->willReturn((object)['UID' => 'someid']); + $this->calendar->expects($this->once()) + ->method('getPermissions') + ->willReturn(Constants::PERMISSION_ALL); + + $backend->expects($this->once()) + ->method('createFromString') + ->with('someid.ics', 'foo'); + $calendarObject->put('foo'); + } + + public function testGet() { + $this->vobject->expects($this->once()) + ->method('serialize') + ->willReturn('foo'); + $this->assertEquals($this->calendarObject->get(), 'foo'); + } + + public function testDelete_notWriteable() { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + $this->calendarObject->delete(); + } + + public function testDelete_noPermission() { + $backend = $this->createMock(ICreateFromString::class); + $calendarObject = new CalendarObject($this->calendar, $backend, $this->vobject); + + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + $calendarObject->delete(); + } + + public function testDelete() { + $backend = $this->createMock(ICreateFromString::class); + $calendarObject = new CalendarObject($this->calendar, $backend, $this->vobject); + + $components = [(new VCalendar(['VEVENT' => ['UID' => 'someid']]))->getBaseComponent()]; + + $this->calendar->expects($this->once()) + ->method('getPermissions') + ->willReturn(Constants::PERMISSION_DELETE); + $this->vobject->expects($this->once()) + ->method('getBaseComponents') + ->willReturn($components); + $this->vobject->expects($this->once()) + ->method('getBaseComponent') + ->willReturn($components[0]); + + $backend->expects($this->once()) + ->method('createFromString') + ->with('someid.ics', self::callback(fn($data): bool => preg_match('/BEGIN:VEVENT(.|\r\n)+STATUS:CANCELLED/', $data) === 1)); + + $calendarObject->delete(); + } + + public function testGetName() { + $this->vobject->expects($this->exactly(2)) + ->method('getBaseComponent') + ->willReturnOnConsecutiveCalls((object)['UID' => 'someid'], (object)['UID' => 'someid', 'X-FILENAME' => 'real-filename.ics']); + + $this->assertEquals($this->calendarObject->getName(), 'someid.ics'); + $this->assertEquals($this->calendarObject->getName(), 'real-filename.ics'); + } + + public function testSetName() { + $this->expectException(\Sabre\DAV\Exception\Forbidden::class); + $this->calendarObject->setName('Some name'); + } +} diff --git a/apps/dav/tests/unit/Command/DeleteCalendarTest.php b/apps/dav/tests/unit/Command/DeleteCalendarTest.php index dec349006ff2c..1c499dbcc259c 100644 --- a/apps/dav/tests/unit/Command/DeleteCalendarTest.php +++ b/apps/dav/tests/unit/Command/DeleteCalendarTest.php @@ -25,7 +25,7 @@ namespace OCA\DAV\Tests\Command; use OCA\DAV\CalDAV\BirthdayService; -use OCA\DAV\CalDav\CalDavBackend; +use OCA\DAV\CalDAV\CalDavBackend; use OCA\DAV\Command\DeleteCalendar; use OCP\IConfig; use OCP\IL10N;