aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChristoph Wurst <ChristophWurst@users.noreply.github.com>2023-04-26 11:15:01 +0200
committerGitHub <noreply@github.com>2023-04-26 11:15:01 +0200
commit5d29a96b1cbc4f125e80e8985f52f8ded111313f (patch)
treefbddd5ebdac4e506c9b25e48be8cd06b064119a8
parent222d9edc59ac753c6f7107c41718a6fd7afb22e6 (diff)
parent62c4ae78df74daa81f44f13bfc2e08c6c784956f (diff)
downloadnextcloud-server-5d29a96b1cbc4f125e80e8985f52f8ded111313f.tar.gz
nextcloud-server-5d29a96b1cbc4f125e80e8985f52f8ded111313f.zip
Merge pull request #35121 from nextcloud/feat/dav-wrap-app-calendars
Feature: Provide access to app generated calendars through CalDAV
-rw-r--r--apps/dav/composer/composer/autoload_classmap.php3
-rw-r--r--apps/dav/composer/composer/autoload_static.php3
-rw-r--r--apps/dav/lib/AppInfo/Application.php14
-rw-r--r--apps/dav/lib/AppInfo/PluginManager.php5
-rw-r--r--apps/dav/lib/CalDAV/AppCalendar/AppCalendar.php208
-rw-r--r--apps/dav/lib/CalDAV/AppCalendar/AppCalendarPlugin.php74
-rw-r--r--apps/dav/lib/CalDAV/AppCalendar/CalendarObject.php153
-rw-r--r--apps/dav/lib/CalDAV/CalendarProvider.php1
-rw-r--r--apps/dav/tests/unit/AppInfo/PluginManagerTest.php25
-rw-r--r--apps/dav/tests/unit/CalDAV/AppCalendar/AppCalendarTest.php123
-rw-r--r--apps/dav/tests/unit/CalDAV/AppCalendar/CalendarObjectTest.php166
-rw-r--r--apps/dav/tests/unit/Command/DeleteCalendarTest.php2
12 files changed, 758 insertions, 19 deletions
diff --git a/apps/dav/composer/composer/autoload_classmap.php b/apps/dav/composer/composer/autoload_classmap.php
index a9bf60698fd..6745ffe41b4 100644
--- a/apps/dav/composer/composer/autoload_classmap.php
+++ b/apps/dav/composer/composer/autoload_classmap.php
@@ -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',
diff --git a/apps/dav/composer/composer/autoload_static.php b/apps/dav/composer/composer/autoload_static.php
index 48104281cd4..302a424d08e 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 86749862626..10e1130f907 100644
--- a/apps/dav/lib/AppInfo/Application.php
+++ b/apps/dav/lib/AppInfo/Application.php
@@ -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
diff --git a/apps/dav/lib/AppInfo/PluginManager.php b/apps/dav/lib/AppInfo/PluginManager.php
index 0b83d6a9205..828818455f7 100644
--- a/apps/dav/lib/AppInfo/PluginManager.php
+++ b/apps/dav/lib/AppInfo/PluginManager.php
@@ -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
index 00000000000..d67f1f5a816
--- /dev/null
+++ b/apps/dav/lib/CalDAV/AppCalendar/AppCalendar.php
@@ -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
index 00000000000..cdf7cb9059a
--- /dev/null
+++ b/apps/dav/lib/CalDAV/AppCalendar/AppCalendarPlugin.php
@@ -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
index 00000000000..985b137c955
--- /dev/null
+++ b/apps/dav/lib/CalDAV/AppCalendar/CalendarObject.php
@@ -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;
+ }
+}
diff --git a/apps/dav/lib/CalDAV/CalendarProvider.php b/apps/dav/lib/CalDAV/CalendarProvider.php
index 5779111add3..f29c601db2d 100644
--- a/apps/dav/lib/CalDAV/CalendarProvider.php
+++ b/apps/dav/lib/CalDAV/CalendarProvider.php
@@ -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;
diff --git a/apps/dav/tests/unit/AppInfo/PluginManagerTest.php b/apps/dav/tests/unit/AppInfo/PluginManagerTest.php
index 17f8ffda625..67dd0477685 100644
--- a/apps/dav/tests/unit/AppInfo/PluginManagerTest.php
+++ b/apps/dav/tests/unit/AppInfo/PluginManagerTest.php
@@ -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
index 00000000000..78ebf8b67a4
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/AppCalendar/AppCalendarTest.php
@@ -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
index 00000000000..e7bd2cc0b95
--- /dev/null
+++ b/apps/dav/tests/unit/CalDAV/AppCalendar/CalendarObjectTest.php
@@ -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');
+ }
+}
diff --git a/apps/dav/tests/unit/Command/DeleteCalendarTest.php b/apps/dav/tests/unit/Command/DeleteCalendarTest.php
index dec349006ff..1c499dbcc25 100644
--- a/apps/dav/tests/unit/Command/DeleteCalendarTest.php
+++ b/apps/dav/tests/unit/Command/DeleteCalendarTest.php
@@ -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;