From 25f56e75ef9abb029346ebcd90124d1faa86bcb2 Mon Sep 17 00:00:00 2001 From: Richard Steinmetz Date: Tue, 4 Jun 2024 15:58:39 +0200 Subject: [PATCH] fix(caldav): stricter default calendar checks Reject calendars that - are subscriptions - are not writable - are shared with a user - are deleted - don't support VEVENTs Signed-off-by: Richard Steinmetz --- apps/dav/appinfo/v1/caldav.php | 3 +- .../composer/composer/autoload_classmap.php | 1 + .../dav/composer/composer/autoload_static.php | 1 + .../lib/CalDAV/DefaultCalendarValidator.php | 41 +++++ .../InvitationResponseServer.php | 3 +- apps/dav/lib/CalDAV/Schedule/Plugin.php | 19 +- .../dav/lib/Connector/Sabre/ServerFactory.php | 4 +- apps/dav/lib/DAV/CustomPropertiesBackend.php | 9 +- apps/dav/lib/Server.php | 6 +- .../CalDAV/DefaultCalendarValidatorTest.php | 171 ++++++++++++++++++ .../tests/unit/CalDAV/Schedule/PluginTest.php | 16 +- .../Sabre/CustomPropertiesBackendTest.php | 10 +- .../unit/DAV/CustomPropertiesBackendTest.php | 61 ++++++- 13 files changed, 326 insertions(+), 19 deletions(-) create mode 100644 apps/dav/lib/CalDAV/DefaultCalendarValidator.php create mode 100644 apps/dav/tests/unit/CalDAV/DefaultCalendarValidatorTest.php diff --git a/apps/dav/appinfo/v1/caldav.php b/apps/dav/appinfo/v1/caldav.php index f9d6b94c16622..c16c89e1c77f4 100644 --- a/apps/dav/appinfo/v1/caldav.php +++ b/apps/dav/appinfo/v1/caldav.php @@ -29,6 +29,7 @@ use OC\KnownUser\KnownUserService; use OCA\DAV\CalDAV\CalDavBackend; use OCA\DAV\CalDAV\CalendarRoot; +use OCA\DAV\CalDAV\DefaultCalendarValidator; use OCA\DAV\CalDAV\Security\RateLimitingPlugin; use OCA\DAV\CalDAV\Validation\CalDavValidatePlugin; use OCA\DAV\Connector\LegacyDAVACL; @@ -112,7 +113,7 @@ $server->addPlugin(new \Sabre\DAV\Sync\Plugin()); $server->addPlugin(new \Sabre\CalDAV\ICSExportPlugin()); -$server->addPlugin(new \OCA\DAV\CalDAV\Schedule\Plugin(\OC::$server->getConfig(), \OC::$server->get(LoggerInterface::class))); +$server->addPlugin(new \OCA\DAV\CalDAV\Schedule\Plugin(\OC::$server->getConfig(), \OC::$server->get(LoggerInterface::class), \OC::$server->get(DefaultCalendarValidator::class))); if ($sendInvitations) { $server->addPlugin(\OC::$server->query(\OCA\DAV\CalDAV\Schedule\IMipPlugin::class)); diff --git a/apps/dav/composer/composer/autoload_classmap.php b/apps/dav/composer/composer/autoload_classmap.php index 13d357bef03a9..3e4dfbcabd112 100644 --- a/apps/dav/composer/composer/autoload_classmap.php +++ b/apps/dav/composer/composer/autoload_classmap.php @@ -58,6 +58,7 @@ 'OCA\\DAV\\CalDAV\\CalendarObject' => $baseDir . '/../lib/CalDAV/CalendarObject.php', 'OCA\\DAV\\CalDAV\\CalendarProvider' => $baseDir . '/../lib/CalDAV/CalendarProvider.php', 'OCA\\DAV\\CalDAV\\CalendarRoot' => $baseDir . '/../lib/CalDAV/CalendarRoot.php', + 'OCA\\DAV\\CalDAV\\DefaultCalendarValidator' => $baseDir . '/../lib/CalDAV/DefaultCalendarValidator.php', 'OCA\\DAV\\CalDAV\\EventComparisonService' => $baseDir . '/../lib/CalDAV/EventComparisonService.php', 'OCA\\DAV\\CalDAV\\FreeBusy\\FreeBusyGenerator' => $baseDir . '/../lib/CalDAV/FreeBusy/FreeBusyGenerator.php', 'OCA\\DAV\\CalDAV\\ICSExportPlugin\\ICSExportPlugin' => $baseDir . '/../lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php', diff --git a/apps/dav/composer/composer/autoload_static.php b/apps/dav/composer/composer/autoload_static.php index 104909bbfc2a0..23feaf5227a17 100644 --- a/apps/dav/composer/composer/autoload_static.php +++ b/apps/dav/composer/composer/autoload_static.php @@ -73,6 +73,7 @@ class ComposerStaticInitDAV 'OCA\\DAV\\CalDAV\\CalendarObject' => __DIR__ . '/..' . '/../lib/CalDAV/CalendarObject.php', 'OCA\\DAV\\CalDAV\\CalendarProvider' => __DIR__ . '/..' . '/../lib/CalDAV/CalendarProvider.php', 'OCA\\DAV\\CalDAV\\CalendarRoot' => __DIR__ . '/..' . '/../lib/CalDAV/CalendarRoot.php', + 'OCA\\DAV\\CalDAV\\DefaultCalendarValidator' => __DIR__ . '/..' . '/../lib/CalDAV/DefaultCalendarValidator.php', 'OCA\\DAV\\CalDAV\\EventComparisonService' => __DIR__ . '/..' . '/../lib/CalDAV/EventComparisonService.php', 'OCA\\DAV\\CalDAV\\FreeBusy\\FreeBusyGenerator' => __DIR__ . '/..' . '/../lib/CalDAV/FreeBusy/FreeBusyGenerator.php', 'OCA\\DAV\\CalDAV\\ICSExportPlugin\\ICSExportPlugin' => __DIR__ . '/..' . '/../lib/CalDAV/ICSExportPlugin/ICSExportPlugin.php', diff --git a/apps/dav/lib/CalDAV/DefaultCalendarValidator.php b/apps/dav/lib/CalDAV/DefaultCalendarValidator.php new file mode 100644 index 0000000000000..266e07ef25509 --- /dev/null +++ b/apps/dav/lib/CalDAV/DefaultCalendarValidator.php @@ -0,0 +1,41 @@ +isSubscription() + || !$calendar->canWrite() + || $calendar->isShared() + || $calendar->isDeleted()) { + throw new DavException('Calendar is a subscription, not writable, shared or deleted'); + } + + // Calendar must support VEVENTs + $sCCS = '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set'; + $calendarProperties = $calendar->getProperties([$sCCS]); + if (isset($calendarProperties[$sCCS])) { + $supportedComponents = $calendarProperties[$sCCS]->getValue(); + } else { + $supportedComponents = ['VJOURNAL', 'VTODO', 'VEVENT']; + } + if (!in_array('VEVENT', $supportedComponents, true)) { + throw new DavException('Calendar does not support VEVENT components'); + } + } +} diff --git a/apps/dav/lib/CalDAV/InvitationResponse/InvitationResponseServer.php b/apps/dav/lib/CalDAV/InvitationResponse/InvitationResponseServer.php index e92eae2d3f173..b8e76e704b32d 100644 --- a/apps/dav/lib/CalDAV/InvitationResponse/InvitationResponseServer.php +++ b/apps/dav/lib/CalDAV/InvitationResponse/InvitationResponseServer.php @@ -28,6 +28,7 @@ use OCA\DAV\AppInfo\PluginManager; use OCA\DAV\CalDAV\Auth\CustomPrincipalPlugin; use OCA\DAV\CalDAV\Auth\PublicPrincipalPlugin; +use OCA\DAV\CalDAV\DefaultCalendarValidator; use OCA\DAV\Connector\Sabre\AnonymousOptionsPlugin; use OCA\DAV\Connector\Sabre\BlockLegacyClientPlugin; use OCA\DAV\Connector\Sabre\CachingTree; @@ -89,7 +90,7 @@ public function __construct(bool $public = true) { // calendar plugins $this->server->addPlugin(new \OCA\DAV\CalDAV\Plugin()); $this->server->addPlugin(new \Sabre\CalDAV\ICSExportPlugin()); - $this->server->addPlugin(new \OCA\DAV\CalDAV\Schedule\Plugin(\OC::$server->getConfig(), \OC::$server->get(LoggerInterface::class))); + $this->server->addPlugin(new \OCA\DAV\CalDAV\Schedule\Plugin(\OC::$server->getConfig(), \OC::$server->get(LoggerInterface::class), \OC::$server->get(DefaultCalendarValidator::class))); $this->server->addPlugin(new \Sabre\CalDAV\Subscriptions\Plugin()); $this->server->addPlugin(new \Sabre\CalDAV\Notifications\Plugin()); //$this->server->addPlugin(new \OCA\DAV\DAV\Sharing\Plugin($authBackend, \OC::$server->getRequest())); diff --git a/apps/dav/lib/CalDAV/Schedule/Plugin.php b/apps/dav/lib/CalDAV/Schedule/Plugin.php index cec42dc2ea14d..51aa46464876d 100644 --- a/apps/dav/lib/CalDAV/Schedule/Plugin.php +++ b/apps/dav/lib/CalDAV/Schedule/Plugin.php @@ -33,11 +33,13 @@ use OCA\DAV\CalDAV\CalDavBackend; use OCA\DAV\CalDAV\Calendar; use OCA\DAV\CalDAV\CalendarHome; +use OCA\DAV\CalDAV\DefaultCalendarValidator; use OCP\IConfig; use Psr\Log\LoggerInterface; use Sabre\CalDAV\ICalendar; use Sabre\CalDAV\ICalendarObject; use Sabre\CalDAV\Schedule\ISchedulingObject; +use Sabre\DAV\Exception as DavException; use Sabre\DAV\INode; use Sabre\DAV\IProperties; use Sabre\DAV\PropFind; @@ -75,13 +77,15 @@ class Plugin extends \Sabre\CalDAV\Schedule\Plugin { public const CALENDAR_USER_TYPE = '{' . self::NS_CALDAV . '}calendar-user-type'; public const SCHEDULE_DEFAULT_CALENDAR_URL = '{' . Plugin::NS_CALDAV . '}schedule-default-calendar-URL'; private LoggerInterface $logger; + private DefaultCalendarValidator $defaultCalendarValidator; /** * @param IConfig $config */ - public function __construct(IConfig $config, LoggerInterface $logger) { + public function __construct(IConfig $config, LoggerInterface $logger, DefaultCalendarValidator $defaultCalendarValidator) { $this->config = $config; $this->logger = $logger; + $this->defaultCalendarValidator = $defaultCalendarValidator; } /** @@ -384,11 +388,20 @@ public function propFindDefaultCalendarUrl(PropFind $propFind, INode $node) { * - isn't a calendar subscription * - user can write to it (no virtual/3rd-party calendars) * - calendar isn't a share + * - calendar supports VEVENTs */ foreach ($calendarHome->getChildren() as $node) { - if ($node instanceof Calendar && !$node->isSubscription() && $node->canWrite() && !$node->isShared() && !$node->isDeleted()) { - $userCalendars[] = $node; + if (!($node instanceof Calendar)) { + continue; } + + try { + $this->defaultCalendarValidator->validateScheduleDefaultCalendar($node); + } catch (DavException $e) { + continue; + } + + $userCalendars[] = $node; } if (count($userCalendars) > 0) { diff --git a/apps/dav/lib/Connector/Sabre/ServerFactory.php b/apps/dav/lib/Connector/Sabre/ServerFactory.php index 758951c42fff8..3b3c10e58cd6a 100644 --- a/apps/dav/lib/Connector/Sabre/ServerFactory.php +++ b/apps/dav/lib/Connector/Sabre/ServerFactory.php @@ -32,6 +32,7 @@ namespace OCA\DAV\Connector\Sabre; use OCA\DAV\AppInfo\PluginManager; +use OCA\DAV\CalDAV\DefaultCalendarValidator; use OCA\DAV\DAV\ViewOnlyPlugin; use OCA\DAV\Files\BrowserErrorPagePlugin; use OCP\EventDispatcher\IEventDispatcher; @@ -191,7 +192,8 @@ public function createServer(string $baseUri, $server, $objectTree, $this->databaseConnection, - $this->userSession->getUser() + $this->userSession->getUser(), + \OC::$server->get(DefaultCalendarValidator::class), ) ) ); diff --git a/apps/dav/lib/DAV/CustomPropertiesBackend.php b/apps/dav/lib/DAV/CustomPropertiesBackend.php index 48872048ea868..378d27a5399b8 100644 --- a/apps/dav/lib/DAV/CustomPropertiesBackend.php +++ b/apps/dav/lib/DAV/CustomPropertiesBackend.php @@ -27,12 +27,13 @@ namespace OCA\DAV\DAV; use Exception; +use OCA\DAV\CalDAV\Calendar; +use OCA\DAV\CalDAV\DefaultCalendarValidator; use OCA\DAV\Connector\Sabre\Directory; use OCA\DAV\Connector\Sabre\FilesPlugin; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; use OCP\IUser; -use Sabre\CalDAV\ICalendar; use Sabre\DAV\Exception as DavException; use Sabre\DAV\PropertyStorage\Backend\BackendInterface; use Sabre\DAV\PropFind; @@ -154,6 +155,7 @@ class CustomPropertiesBackend implements BackendInterface { private Server $server; private XmlService $xmlService; + private DefaultCalendarValidator $defaultCalendarValidator; /** * @param Tree $tree node tree @@ -165,6 +167,7 @@ public function __construct( Tree $tree, IDBConnection $connection, IUser $user, + DefaultCalendarValidator $defaultCalendarValidator, ) { $this->server = $server; $this->tree = $tree; @@ -175,6 +178,7 @@ public function __construct( $this->xmlService->elementMap, self::COMPLEX_XML_ELEMENT_MAP, ); + $this->defaultCalendarValidator = $defaultCalendarValidator; } /** @@ -338,10 +342,11 @@ private function validateProperty(string $path, string $propName, mixed $propVal // $path is the principal here as this prop is only set on principals $node = $this->tree->getNodeForPath($href); - if (!($node instanceof ICalendar) || $node->getOwner() !== $path) { + if (!($node instanceof Calendar) || $node->getOwner() !== $path) { throw new DavException('No such calendar'); } + $this->defaultCalendarValidator->validateScheduleDefaultCalendar($node); break; } } diff --git a/apps/dav/lib/Server.php b/apps/dav/lib/Server.php index 6ab24f55adb69..11fa61015e88d 100644 --- a/apps/dav/lib/Server.php +++ b/apps/dav/lib/Server.php @@ -39,6 +39,7 @@ use OCA\DAV\AppInfo\PluginManager; use OCA\DAV\BulkUpload\BulkUploadPlugin; use OCA\DAV\CalDAV\BirthdayService; +use OCA\DAV\CalDAV\DefaultCalendarValidator; use OCA\DAV\CalDAV\Security\RateLimitingPlugin; use OCA\DAV\CalDAV\Validation\CalDavValidatePlugin; use OCA\DAV\CardDAV\HasPhotoPlugin; @@ -181,7 +182,7 @@ public function __construct(IRequest $request, string $baseUri) { if ($this->requestIsForSubtree(['calendars', 'public-calendars', 'system-calendars', 'principals'])) { $this->server->addPlugin(new \OCA\DAV\CalDAV\Plugin()); $this->server->addPlugin(new \OCA\DAV\CalDAV\ICSExportPlugin\ICSExportPlugin(\OC::$server->getConfig(), $logger)); - $this->server->addPlugin(new \OCA\DAV\CalDAV\Schedule\Plugin(\OC::$server->getConfig(), \OC::$server->get(LoggerInterface::class))); + $this->server->addPlugin(new \OCA\DAV\CalDAV\Schedule\Plugin(\OC::$server->getConfig(), \OC::$server->get(LoggerInterface::class), \OC::$server->get(DefaultCalendarValidator::class))); if (\OC::$server->getConfig()->getAppValue('dav', 'sendInvitations', 'yes') === 'yes') { $this->server->addPlugin(\OC::$server->query(\OCA\DAV\CalDAV\Schedule\IMipPlugin::class)); } @@ -286,7 +287,8 @@ public function __construct(IRequest $request, string $baseUri) { $this->server, $this->server->tree, \OC::$server->getDatabaseConnection(), - \OC::$server->getUserSession()->getUser() + \OC::$server->getUserSession()->getUser(), + \OC::$server->get(DefaultCalendarValidator::class), ) ) ); diff --git a/apps/dav/tests/unit/CalDAV/DefaultCalendarValidatorTest.php b/apps/dav/tests/unit/CalDAV/DefaultCalendarValidatorTest.php new file mode 100644 index 0000000000000..194009827dad6 --- /dev/null +++ b/apps/dav/tests/unit/CalDAV/DefaultCalendarValidatorTest.php @@ -0,0 +1,171 @@ +validator = new DefaultCalendarValidator(); + } + + public function testValidateScheduleDefaultCalendar(): void { + $node = $this->createMock(Calendar::class); + $node->expects(self::once()) + ->method('isSubscription') + ->willReturn(false); + $node->expects(self::once()) + ->method('canWrite') + ->willReturn(true); + $node->expects(self::once()) + ->method('isShared') + ->willReturn(false); + $node->expects(self::once()) + ->method('isDeleted') + ->willReturn(false); + $node->expects(self::once()) + ->method('getProperties') + ->willReturn([ + '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VEVENT']), + ]); + + $this->validator->validateScheduleDefaultCalendar($node); + } + + public function testValidateScheduleDefaultCalendarWithEmptyProperties(): void { + $node = $this->createMock(Calendar::class); + $node->expects(self::once()) + ->method('isSubscription') + ->willReturn(false); + $node->expects(self::once()) + ->method('canWrite') + ->willReturn(true); + $node->expects(self::once()) + ->method('isShared') + ->willReturn(false); + $node->expects(self::once()) + ->method('isDeleted') + ->willReturn(false); + $node->expects(self::once()) + ->method('getProperties') + ->willReturn([]); + + $this->validator->validateScheduleDefaultCalendar($node); + } + + public function testValidateScheduleDefaultCalendarWithSubscription(): void { + $node = $this->createMock(Calendar::class); + $node->expects(self::once()) + ->method('isSubscription') + ->willReturn(true); + $node->expects(self::never()) + ->method('canWrite'); + $node->expects(self::never()) + ->method('isShared'); + $node->expects(self::never()) + ->method('isDeleted'); + $node->expects(self::never()) + ->method('getProperties'); + + $this->expectException(\Sabre\DAV\Exception::class); + $this->validator->validateScheduleDefaultCalendar($node); + } + + public function testValidateScheduleDefaultCalendarWithoutWrite(): void { + $node = $this->createMock(Calendar::class); + $node->expects(self::once()) + ->method('isSubscription') + ->willReturn(false); + $node->expects(self::once()) + ->method('canWrite') + ->willReturn(false); + $node->expects(self::never()) + ->method('isShared'); + $node->expects(self::never()) + ->method('isDeleted'); + $node->expects(self::never()) + ->method('getProperties'); + + $this->expectException(\Sabre\DAV\Exception::class); + $this->validator->validateScheduleDefaultCalendar($node); + } + + public function testValidateScheduleDefaultCalendarWithShared(): void { + $node = $this->createMock(Calendar::class); + $node->expects(self::once()) + ->method('isSubscription') + ->willReturn(false); + $node->expects(self::once()) + ->method('canWrite') + ->willReturn(true); + $node->expects(self::once()) + ->method('isShared') + ->willReturn(true); + $node->expects(self::never()) + ->method('isDeleted'); + $node->expects(self::never()) + ->method('getProperties'); + + $this->expectException(\Sabre\DAV\Exception::class); + $this->validator->validateScheduleDefaultCalendar($node); + } + + public function testValidateScheduleDefaultCalendarWithDeleted(): void { + $node = $this->createMock(Calendar::class); + $node->expects(self::once()) + ->method('isSubscription') + ->willReturn(false); + $node->expects(self::once()) + ->method('canWrite') + ->willReturn(true); + $node->expects(self::once()) + ->method('isShared') + ->willReturn(false); + $node->expects(self::once()) + ->method('isDeleted') + ->willReturn(true); + $node->expects(self::never()) + ->method('getProperties'); + + $this->expectException(\Sabre\DAV\Exception::class); + $this->validator->validateScheduleDefaultCalendar($node); + } + + public function testValidateScheduleDefaultCalendarWithoutVeventSupport(): void { + $node = $this->createMock(Calendar::class); + $node->expects(self::once()) + ->method('isSubscription') + ->willReturn(false); + $node->expects(self::once()) + ->method('canWrite') + ->willReturn(true); + $node->expects(self::once()) + ->method('isShared') + ->willReturn(false); + $node->expects(self::once()) + ->method('isDeleted') + ->willReturn(false); + $node->expects(self::once()) + ->method('getProperties') + ->willReturn([ + '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set' => new SupportedCalendarComponentSet(['VTODO']), + ]); + + $this->expectException(\Sabre\DAV\Exception::class); + $this->validator->validateScheduleDefaultCalendar($node); + } +} diff --git a/apps/dav/tests/unit/CalDAV/Schedule/PluginTest.php b/apps/dav/tests/unit/CalDAV/Schedule/PluginTest.php index 8f315eac0ee52..f3a00b6ccf251 100644 --- a/apps/dav/tests/unit/CalDAV/Schedule/PluginTest.php +++ b/apps/dav/tests/unit/CalDAV/Schedule/PluginTest.php @@ -29,6 +29,7 @@ use OCA\DAV\CalDAV\CalDavBackend; use OCA\DAV\CalDAV\Calendar; use OCA\DAV\CalDAV\CalendarHome; +use OCA\DAV\CalDAV\DefaultCalendarValidator; use OCA\DAV\CalDAV\Plugin as CalDAVPlugin; use OCA\DAV\CalDAV\Schedule\Plugin; use OCA\DAV\CalDAV\Trashbin\Plugin as TrashbinPlugin; @@ -60,6 +61,9 @@ class PluginTest extends TestCase { /** @var MockObject|LoggerInterface */ private $logger; + /** @property MockObject|DefaultCalendarValidator */ + private $defaultCalendarValidator; + protected function setUp(): void { parent::setUp(); @@ -74,13 +78,14 @@ protected function setUp(): void { $this->server->xml = new Service(); $this->logger = $this->createMock(LoggerInterface::class); + $this->defaultCalendarValidator = $this->createMock(DefaultCalendarValidator::class); - $this->plugin = new Plugin($this->config, $this->logger); + $this->plugin = new Plugin($this->config, $this->logger, $this->defaultCalendarValidator); $this->plugin->initialize($this->server); } public function testInitialize(): void { - $plugin = new Plugin($this->config, $this->logger); + $plugin = new Plugin($this->config, $this->logger, $this->defaultCalendarValidator); $this->server->expects($this->exactly(10)) ->method('on') @@ -397,6 +402,13 @@ public function testPropFindDefaultCalendarUrl(string $principalUri, ?string $ca ->with($calendarHome .'/' . $calendarUri, [], 1) ->willReturn($properties); + $this->defaultCalendarValidator->method('validateScheduleDefaultCalendar') + ->willReturnCallback(function (Calendar $node) { + if ($node->isDeleted()) { + throw new \Sabre\DAV\Exception('Deleted calendar'); + } + }); + $this->plugin->propFindDefaultCalendarUrl($propFind, $node); if (!$propertiesForPath) { diff --git a/apps/dav/tests/unit/Connector/Sabre/CustomPropertiesBackendTest.php b/apps/dav/tests/unit/Connector/Sabre/CustomPropertiesBackendTest.php index 636fd0d2d8d6e..5a3f66e30cefe 100644 --- a/apps/dav/tests/unit/Connector/Sabre/CustomPropertiesBackendTest.php +++ b/apps/dav/tests/unit/Connector/Sabre/CustomPropertiesBackendTest.php @@ -36,9 +36,11 @@ * See the COPYING-README file. */ +use OCA\DAV\CalDAV\DefaultCalendarValidator; use OCA\DAV\Connector\Sabre\Directory; use OCA\DAV\Connector\Sabre\File; use OCP\IUser; +use PHPUnit\Framework\MockObject\MockObject; use Sabre\DAV\Tree; /** @@ -70,6 +72,9 @@ class CustomPropertiesBackendTest extends \Test\TestCase { */ private $user; + /** @property MockObject|DefaultCalendarValidator */ + private $defaultCalendarValidator; + protected function setUp(): void { parent::setUp(); $this->server = new \Sabre\DAV\Server(); @@ -86,11 +91,14 @@ protected function setUp(): void { ->method('getUID') ->willReturn($userId); + $this->defaultCalendarValidator = $this->createMock(DefaultCalendarValidator::class); + $this->plugin = new \OCA\DAV\DAV\CustomPropertiesBackend( $this->server, $this->tree, \OC::$server->getDatabaseConnection(), - $this->user + $this->user, + $this->defaultCalendarValidator, ); } diff --git a/apps/dav/tests/unit/DAV/CustomPropertiesBackendTest.php b/apps/dav/tests/unit/DAV/CustomPropertiesBackendTest.php index b81d7f24ae4a1..d87139c4a901e 100644 --- a/apps/dav/tests/unit/DAV/CustomPropertiesBackendTest.php +++ b/apps/dav/tests/unit/DAV/CustomPropertiesBackendTest.php @@ -28,11 +28,12 @@ */ namespace OCA\DAV\Tests\DAV; +use OCA\DAV\CalDAV\Calendar; +use OCA\DAV\CalDAV\DefaultCalendarValidator; use OCA\DAV\DAV\CustomPropertiesBackend; use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; use OCP\IUser; -use Sabre\CalDAV\ICalendar; use Sabre\DAV\Exception\NotFound; use Sabre\DAV\PropFind; use Sabre\DAV\PropPatch; @@ -64,6 +65,9 @@ class CustomPropertiesBackendTest extends TestCase { /** @var CustomPropertiesBackend | \PHPUnit\Framework\MockObject\MockObject */ private $backend; + /** @property DefaultCalendarValidator | \PHPUnit\Framework\MockObject\MockObject */ + private $defaultCalendarValidator; + protected function setUp(): void { parent::setUp(); @@ -76,12 +80,14 @@ protected function setUp(): void { ->with() ->willReturn('dummy_user_42'); $this->dbConnection = \OC::$server->getDatabaseConnection(); + $this->defaultCalendarValidator = $this->createMock(DefaultCalendarValidator::class); $this->backend = new CustomPropertiesBackend( $this->server, $this->tree, $this->dbConnection, $this->user, + $this->defaultCalendarValidator, ); } @@ -154,6 +160,7 @@ public function testPropFindNoDbCalls(): void { $this->tree, $db, $this->user, + $this->defaultCalendarValidator, ); $propFind = $this->createMock(PropFind::class); @@ -218,7 +225,7 @@ public function testPropFindCalendarCall(): void { public function testPropFindPrincipalCall(): void { $this->tree->method('getNodeForPath') ->willReturnCallback(function ($uri) { - $node = $this->createMock(ICalendar::class); + $node = $this->createMock(Calendar::class); $node->method('getOwner') ->willReturn('principals/users/dummy_user_42'); return $node; @@ -265,21 +272,21 @@ public function propFindPrincipalScheduleDefaultCalendarProviderUrlProvider(): a return [ [ // Exists 'dummy_user_42', - ['calendars/dummy_user_42/foo/' => ICalendar::class], + ['calendars/dummy_user_42/foo/' => Calendar::class], ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('calendars/dummy_user_42/foo/')], ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL'], ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('calendars/dummy_user_42/foo/')], ], [ // Doesn't exist 'dummy_user_42', - ['calendars/dummy_user_42/foo/' => ICalendar::class], + ['calendars/dummy_user_42/foo/' => Calendar::class], ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('calendars/dummy_user_42/bar/')], ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL'], [], ], [ // No privilege 'dummy_user_42', - ['calendars/user2/baz/' => ICalendar::class], + ['calendars/user2/baz/' => Calendar::class], ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('calendars/user2/baz/')], ['{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL'], [], @@ -372,7 +379,7 @@ public function testPropPatch(string $path, array $existing, array $props, array }); $this->tree->method('getNodeForPath') ->willReturnCallback(function ($uri) { - $node = $this->createMock(ICalendar::class); + $node = $this->createMock(Calendar::class); $node->method('getOwner') ->willReturn('principals/users/' . $this->user->getUID()); return $node; @@ -400,6 +407,48 @@ public function propPatchProvider() { ]; } + public function testPropPatchWithUnsuitableCalendar(): void { + $path = 'principals/users/' . $this->user->getUID(); + + $node = $this->createMock(Calendar::class); + $node->expects(self::once()) + ->method('getOwner') + ->willReturn($path); + + $this->defaultCalendarValidator->expects(self::once()) + ->method('validateScheduleDefaultCalendar') + ->with($node) + ->willThrowException(new \Sabre\DAV\Exception("Invalid calendar")); + + $this->server->method('calculateUri') + ->willReturnCallback(function ($uri) { + if (str_starts_with($uri, self::BASE_URI)) { + return trim(substr($uri, strlen(self::BASE_URI)), '/'); + } + return null; + }); + $this->tree->expects(self::once()) + ->method('getNodeForPath') + ->with('foo/bar/') + ->willReturn($node); + + $storedProps = $this->getProps($this->user->getUID(), $path); + $this->assertEquals([], $storedProps); + + $propPatch = new PropPatch([ + '{urn:ietf:params:xml:ns:caldav}schedule-default-calendar-URL' => new Href('foo/bar/'), + ]); + $this->backend->propPatch($path, $propPatch); + try { + $propPatch->commit(); + } catch (\Throwable $e) { + $this->assertInstanceOf(\Sabre\DAV\Exception::class, $e); + } + + $storedProps = $this->getProps($this->user->getUID(), $path); + $this->assertEquals([], $storedProps); + } + /** * @dataProvider deleteProvider */