]> source.dussan.org Git - nextcloud-server.git/commitdiff
Feature: Provide access to app generated calendars through CalDAV 35121/head
authorFerdinand Thiessen <rpm@fthiessen.de>
Sat, 12 Nov 2022 16:42:12 +0000 (17:42 +0100)
committerFerdinand Thiessen <opensource@fthiessen.de>
Tue, 25 Apr 2023 16:11:49 +0000 (18:11 +0200)
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>
12 files changed:
apps/dav/composer/composer/autoload_classmap.php
apps/dav/composer/composer/autoload_static.php
apps/dav/lib/AppInfo/Application.php
apps/dav/lib/AppInfo/PluginManager.php
apps/dav/lib/CalDAV/AppCalendar/AppCalendar.php [new file with mode: 0644]
apps/dav/lib/CalDAV/AppCalendar/AppCalendarPlugin.php [new file with mode: 0644]
apps/dav/lib/CalDAV/AppCalendar/CalendarObject.php [new file with mode: 0644]
apps/dav/lib/CalDAV/CalendarProvider.php
apps/dav/tests/unit/AppInfo/PluginManagerTest.php
apps/dav/tests/unit/CalDAV/AppCalendar/AppCalendarTest.php [new file with mode: 0644]
apps/dav/tests/unit/CalDAV/AppCalendar/CalendarObjectTest.php [new file with mode: 0644]
apps/dav/tests/unit/Command/DeleteCalendarTest.php

index a9bf60698fdb833dcd15cc6d4b53d7015f4df28f..6745ffe41b4517eaed6ef018199353e622c20e6a 100644 (file)
@@ -37,6 +37,9 @@ return array(
     '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',
index 48104281cd4b23350a01d3a14baf823ace4a9163..302a424d08ea634976abeb9f0b4718b362908483 100644 (file)
@@ -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',
index 8674986262623d19324178580714c34fb4c8010c..10e1130f907837ad0b9b9d0d64c8f8bc120eeb69 100644 (file)
@@ -35,6 +35,7 @@ namespace OCA\DAV\AppInfo;
 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\NotificationProviderManager;
 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\Calendar\IManager as ICalendarManager;
 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 @@ class Application extends App implements IBootstrap {
        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
index 0b83d6a9205a1348af473eb9e72ed2b92dcf62b2..828818455f75bb9fa2353eae080654dbe027eb4a 100644 (file)
@@ -29,6 +29,7 @@ declare(strict_types=1);
 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 @@ class PluginManager {
                }
                $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 @@ class PluginManager {
 
        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 (file)
index 0000000..d67f1f5
--- /dev/null
@@ -0,0 +1,208 @@
+<?php
+declare(strict_types=1);
+
+/**
+ * @copyright 2023 Ferdinand Thiessen <opensource@fthiessen.de>
+ *
+ * @author Ferdinand Thiessen <opensource@fthiessen.de>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+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 (file)
index 0000000..cdf7cb9
--- /dev/null
@@ -0,0 +1,74 @@
+<?php
+declare(strict_types=1);
+
+/**
+ * @copyright 2023 Ferdinand Thiessen <opensource@fthiessen.de>
+ *
+ * @author Ferdinand Thiessen <opensource@fthiessen.de>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+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 (file)
index 0000000..985b137
--- /dev/null
@@ -0,0 +1,153 @@
+<?php
+declare(strict_types=1);
+
+/**
+ * @copyright 2023 Ferdinand Thiessen <opensource@fthiessen.de>
+ *
+ * @author Ferdinand Thiessen <opensource@fthiessen.de>
+ *
+ * @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 <http://www.gnu.org/licenses/>.
+ *
+ */
+
+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;
+       }
+}
index 5779111add3ade5ade5102abd7a49b8572357e4c..f29c601db2d27a2441bfa89d9edcfc2aa410282a 100644 (file)
@@ -26,7 +26,6 @@ declare(strict_types=1);
 namespace OCA\DAV\CalDAV;
 
 use OCP\Calendar\ICalendarProvider;
-use OCP\Calendar\ICreateFromString;
 use OCP\IConfig;
 use OCP\IL10N;
 use Psr\Log\LoggerInterface;
index 17f8ffda6258049d8b722fef473cfef9dda5b94d..67dd047768540009aa03693b857ada74385ec5c0 100644 (file)
@@ -29,6 +29,7 @@ namespace OCA\DAV\Tests\unit\AppInfo;
 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 @@ class PluginManagerTest extends TestCase {
 
                $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 @@ class PluginManagerTest extends TestCase {
                $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 @@ class PluginManagerTest extends TestCase {
                        $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 (file)
index 0000000..78ebf8b
--- /dev/null
@@ -0,0 +1,123 @@
+<?php
+/**
+ * @license GNU AGPL version 3 or any later version
+ *
+ * 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 <http://www.gnu.org/licenses/>.
+ *
+ */
+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 (file)
index 0000000..e7bd2cc
--- /dev/null
@@ -0,0 +1,166 @@
+<?php
+
+namespace OCA\DAV\Tests\unit\CalDAV\AppCalendar;
+
+use OCA\DAV\CalDAV\AppCalendar\AppCalendar;
+use OCA\DAV\CalDAV\AppCalendar\CalendarObject;
+use OCP\Calendar\ICalendar;
+use OCP\Calendar\ICreateFromString;
+use OCP\Constants;
+use PHPUnit\Framework\MockObject\MockObject;
+use Sabre\VObject\Component\VCalendar;
+use Sabre\VObject\Component\VEvent;
+use Test\TestCase;
+
+class CalendarObjectTest extends TestCase {
+       private CalendarObject $calendarObject;
+       private AppCalendar|MockObject $calendar;
+       private ICalendar|MockObject $backend;
+       private VCalendar|MockObject $vobject;
+
+       protected function setUp(): void {
+               parent::setUp();
+
+               $this->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');
+       }
+}
index dec349006ff2c00e3849d294ec134daf7ab8a866..1c499dbcc259cd001e4d49a7a0a2e56714470da1 100644 (file)
@@ -25,7 +25,7 @@ declare(strict_types=1);
 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;