diff options
author | Jasper Weyne <jasperweyne@gmail.com> | 2022-08-11 09:54:08 +0300 |
---|---|---|
committer | GitHub <noreply@github.com> | 2022-08-11 09:54:08 +0300 |
commit | 44f6c931e7c9c74ea4f448d3cdfbaa89f3b7c379 (patch) | |
tree | 710a8c1bd1c20c685991de146aa9ef149ec1de7a /apps/dav/lib | |
parent | 0633a1d9f5a7ef06d577ae6556d09db9e94f5684 (diff) | |
parent | a61331f4560468e6d433cf32e008b157b06e7ea9 (diff) |
Merge branch 'master' into patch-2
Diffstat (limited to 'apps/dav/lib')
63 files changed, 1864 insertions, 770 deletions
diff --git a/apps/dav/lib/AppInfo/Application.php b/apps/dav/lib/AppInfo/Application.php index 580918a6450..86749862626 100644 --- a/apps/dav/lib/AppInfo/Application.php +++ b/apps/dav/lib/AppInfo/Application.php @@ -35,18 +35,14 @@ namespace OCA\DAV\AppInfo; use Exception; use OCA\DAV\BackgroundJob\UpdateCalendarResourcesRoomsBackgroundJob; use OCA\DAV\CalDAV\Activity\Backend; -use OCA\DAV\CalDAV\BirthdayService; -use OCA\DAV\CalDAV\CalDavBackend; use OCA\DAV\CalDAV\CalendarManager; use OCA\DAV\CalDAV\CalendarProvider; -use OCA\DAV\CalDAV\Reminder\Backend as ReminderBackend; use OCA\DAV\CalDAV\Reminder\NotificationProvider\AudioProvider; use OCA\DAV\CalDAV\Reminder\NotificationProvider\EmailProvider; use OCA\DAV\CalDAV\Reminder\NotificationProvider\PushProvider; use OCA\DAV\CalDAV\Reminder\NotificationProviderManager; use OCA\DAV\CalDAV\Reminder\Notifier; -use OCA\DAV\CalDAV\WebcalCaching\RefreshWebcalService; use OCA\DAV\Capabilities; use OCA\DAV\CardDAV\CardDavBackend; use OCA\DAV\CardDAV\ContactsManager; @@ -61,22 +57,35 @@ use OCA\DAV\Events\CalendarDeletedEvent; use OCA\DAV\Events\CalendarMovedToTrashEvent; use OCA\DAV\Events\CalendarObjectCreatedEvent; use OCA\DAV\Events\CalendarObjectDeletedEvent; +use OCA\DAV\Events\CalendarObjectMovedEvent; use OCA\DAV\Events\CalendarObjectMovedToTrashEvent; use OCA\DAV\Events\CalendarObjectRestoredEvent; use OCA\DAV\Events\CalendarObjectUpdatedEvent; +use OCA\DAV\Events\CalendarPublishedEvent; use OCA\DAV\Events\CalendarRestoredEvent; use OCA\DAV\Events\CalendarShareUpdatedEvent; +use OCA\DAV\Events\CalendarUnpublishedEvent; use OCA\DAV\Events\CalendarUpdatedEvent; use OCA\DAV\Events\CardCreatedEvent; use OCA\DAV\Events\CardDeletedEvent; use OCA\DAV\Events\CardUpdatedEvent; +use OCA\DAV\Events\SubscriptionCreatedEvent; +use OCA\DAV\Events\SubscriptionDeletedEvent; +use OCP\Federation\Events\TrustedServerRemovedEvent; use OCA\DAV\HookManager; use OCA\DAV\Listener\ActivityUpdaterListener; use OCA\DAV\Listener\AddressbookListener; +use OCA\DAV\Listener\BirthdayListener; use OCA\DAV\Listener\CalendarContactInteractionListener; use OCA\DAV\Listener\CalendarDeletionDefaultUpdaterListener; use OCA\DAV\Listener\CalendarObjectReminderUpdaterListener; +use OCA\DAV\Listener\CalendarPublicationListener; +use OCA\DAV\Listener\CalendarShareUpdateListener; use OCA\DAV\Listener\CardListener; +use OCA\DAV\Listener\ClearPhotoCacheListener; +use OCA\DAV\Listener\SubscriptionListener; +use OCA\DAV\Listener\TrustedServerRemovedListener; +use OCA\DAV\Listener\UserPreferenceListener; use OCA\DAV\Search\ContactsSearchProvider; use OCA\DAV\Search\EventsSearchProvider; use OCA\DAV\Search\TasksSearchProvider; @@ -88,6 +97,8 @@ use OCP\AppFramework\Bootstrap\IBootstrap; use OCP\AppFramework\Bootstrap\IRegistrationContext; use OCP\AppFramework\IAppContainer; use OCP\Calendar\IManager as ICalendarManager; +use OCP\Config\BeforePreferenceDeletedEvent; +use OCP\Config\BeforePreferenceSetEvent; use OCP\Contacts\IManager as IContactsManager; use OCP\IServerContainer; use OCP\IUser; @@ -149,11 +160,19 @@ class Application extends App implements IBootstrap { $context->registerEventListener(CalendarObjectUpdatedEvent::class, CalendarObjectReminderUpdaterListener::class); $context->registerEventListener(CalendarObjectDeletedEvent::class, ActivityUpdaterListener::class); $context->registerEventListener(CalendarObjectDeletedEvent::class, CalendarObjectReminderUpdaterListener::class); + $context->registerEventListener(CalendarObjectMovedEvent::class, ActivityUpdaterListener::class); + $context->registerEventListener(CalendarObjectMovedEvent::class, CalendarObjectReminderUpdaterListener::class); $context->registerEventListener(CalendarObjectMovedToTrashEvent::class, ActivityUpdaterListener::class); $context->registerEventListener(CalendarObjectMovedToTrashEvent::class, CalendarObjectReminderUpdaterListener::class); $context->registerEventListener(CalendarObjectRestoredEvent::class, ActivityUpdaterListener::class); $context->registerEventListener(CalendarObjectRestoredEvent::class, CalendarObjectReminderUpdaterListener::class); $context->registerEventListener(CalendarShareUpdatedEvent::class, CalendarContactInteractionListener::class); + $context->registerEventListener(CalendarPublishedEvent::class, CalendarPublicationListener::class); + $context->registerEventListener(CalendarUnpublishedEvent::class, CalendarPublicationListener::class); + $context->registerEventListener(CalendarShareUpdatedEvent::class, CalendarShareUpdateListener::class); + + $context->registerEventListener(SubscriptionCreatedEvent::class, SubscriptionListener::class); + $context->registerEventListener(SubscriptionDeletedEvent::class, SubscriptionListener::class); $context->registerEventListener(AddressBookCreatedEvent::class, AddressbookListener::class); @@ -163,6 +182,15 @@ class Application extends App implements IBootstrap { $context->registerEventListener(CardCreatedEvent::class, CardListener::class); $context->registerEventListener(CardDeletedEvent::class, CardListener::class); $context->registerEventListener(CardUpdatedEvent::class, CardListener::class); + $context->registerEventListener(CardCreatedEvent::class, BirthdayListener::class); + $context->registerEventListener(CardDeletedEvent::class, BirthdayListener::class); + $context->registerEventListener(CardUpdatedEvent::class, BirthdayListener::class); + $context->registerEventListener(CardDeletedEvent::class, ClearPhotoCacheListener::class); + $context->registerEventListener(CardUpdatedEvent::class, ClearPhotoCacheListener::class); + $context->registerEventListener(TrustedServerRemovedEvent::class, TrustedServerRemovedListener::class); + + $context->registerEventListener(BeforePreferenceDeletedEvent::class, UserPreferenceListener::class); + $context->registerEventListener(BeforePreferenceSetEvent::class, UserPreferenceListener::class); $context->registerNotifierService(Notifier::class); @@ -195,44 +223,6 @@ class Application extends App implements IBootstrap { } }); - $birthdayListener = function ($event) use ($container): void { - if ($event instanceof GenericEvent) { - /** @var BirthdayService $b */ - $b = $container->query(BirthdayService::class); - $b->onCardChanged( - (int) $event->getArgument('addressBookId'), - (string) $event->getArgument('cardUri'), - (string) $event->getArgument('cardData') - ); - } - }; - - $dispatcher->addListener('\OCA\DAV\CardDAV\CardDavBackend::createCard', $birthdayListener); - $dispatcher->addListener('\OCA\DAV\CardDAV\CardDavBackend::updateCard', $birthdayListener); - $dispatcher->addListener('\OCA\DAV\CardDAV\CardDavBackend::deleteCard', function ($event) use ($container) { - if ($event instanceof GenericEvent) { - /** @var BirthdayService $b */ - $b = $container->query(BirthdayService::class); - $b->onCardDeleted( - (int) $event->getArgument('addressBookId'), - (string) $event->getArgument('cardUri') - ); - } - }); - - $clearPhotoCache = function ($event) use ($container): void { - if ($event instanceof GenericEvent) { - /** @var PhotoCache $p */ - $p = $container->query(PhotoCache::class); - $p->delete( - $event->getArgument('addressBookId'), - $event->getArgument('cardUri') - ); - } - }; - $dispatcher->addListener('\OCA\DAV\CardDAV\CardDavBackend::updateCard', $clearPhotoCache); - $dispatcher->addListener('\OCA\DAV\CardDAV\CardDavBackend::deleteCard', $clearPhotoCache); - $dispatcher->addListener('OC\AccountManager::userUpdated', function (GenericEvent $event) use ($container) { $user = $event->getSubject(); /** @var SyncService $syncService */ @@ -254,70 +244,6 @@ class Application extends App implements IBootstrap { // Here we should recalculate if reminders should be sent to new or old sharees }); - $dispatcher->addListener('\OCA\DAV\CalDAV\CalDavBackend::publishCalendar', function (GenericEvent $event) use ($container) { - /** @var Backend $backend */ - $backend = $container->query(Backend::class); - $backend->onCalendarPublication( - $event->getArgument('calendarData'), - $event->getArgument('public') - ); - }); - - - $dispatcher->addListener('OCP\Federation\TrustedServerEvent::remove', - function (GenericEvent $event) { - /** @var CardDavBackend $cardDavBackend */ - $cardDavBackend = \OC::$server->query(CardDavBackend::class); - $addressBookUri = $event->getSubject(); - $addressBook = $cardDavBackend->getAddressBooksByUri('principals/system/system', $addressBookUri); - if (!is_null($addressBook)) { - $cardDavBackend->deleteAddressBook($addressBook['id']); - } - } - ); - - $dispatcher->addListener('\OCA\DAV\CalDAV\CalDavBackend::createSubscription', - function (GenericEvent $event) use ($container, $serverContainer) { - $jobList = $serverContainer->getJobList(); - $subscriptionData = $event->getArgument('subscriptionData'); - - /** - * Initial subscription refetch - * - * @var RefreshWebcalService $refreshWebcalService - */ - $refreshWebcalService = $container->query(RefreshWebcalService::class); - $refreshWebcalService->refreshSubscription( - (string) $subscriptionData['principaluri'], - (string) $subscriptionData['uri'] - ); - - $jobList->add(\OCA\DAV\BackgroundJob\RefreshWebcalJob::class, [ - 'principaluri' => $subscriptionData['principaluri'], - 'uri' => $subscriptionData['uri'] - ]); - } - ); - - $dispatcher->addListener('\OCA\DAV\CalDAV\CalDavBackend::deleteSubscription', - function (GenericEvent $event) use ($container, $serverContainer) { - $jobList = $serverContainer->getJobList(); - $subscriptionData = $event->getArgument('subscriptionData'); - - $jobList->remove(\OCA\DAV\BackgroundJob\RefreshWebcalJob::class, [ - 'principaluri' => $subscriptionData['principaluri'], - 'uri' => $subscriptionData['uri'] - ]); - - /** @var CalDavBackend $calDavBackend */ - $calDavBackend = $container->get(CalDavBackend::class); - $calDavBackend->purgeAllCachedEventsForSubscription($subscriptionData['id']); - /** @var ReminderBackend $calDavBackend */ - $reminderBackend = $container->get(ReminderBackend::class); - $reminderBackend->cleanRemindersForCalendar((int) $subscriptionData['id']); - } - ); - $eventHandler = function () use ($container, $serverContainer): void { try { /** @var UpdateCalendarResourcesRoomsBackgroundJob $job */ diff --git a/apps/dav/lib/BackgroundJob/UploadCleanup.php b/apps/dav/lib/BackgroundJob/UploadCleanup.php index f612f58cd7c..3a6f296deaf 100644 --- a/apps/dav/lib/BackgroundJob/UploadCleanup.php +++ b/apps/dav/lib/BackgroundJob/UploadCleanup.php @@ -37,15 +37,18 @@ use OCP\Files\File; use OCP\Files\Folder; use OCP\Files\IRootFolder; use OCP\Files\NotFoundException; +use Psr\Log\LoggerInterface; class UploadCleanup extends TimedJob { private IRootFolder $rootFolder; private IJobList $jobList; + private LoggerInterface $logger; - public function __construct(ITimeFactory $time, IRootFolder $rootFolder, IJobList $jobList) { + public function __construct(ITimeFactory $time, IRootFolder $rootFolder, IJobList $jobList, LoggerInterface $logger) { parent::__construct($time); $this->rootFolder = $rootFolder; $this->jobList = $jobList; + $this->logger = $logger; // Run once a day $this->setInterval(60 * 60 * 24); @@ -61,19 +64,27 @@ class UploadCleanup extends TimedJob { $userRoot = $userFolder->getParent(); /** @var Folder $uploads */ $uploads = $userRoot->get('uploads'); - /** @var Folder $uploadFolder */ $uploadFolder = $uploads->get($folder); } catch (NotFoundException | NoUserException $e) { $this->jobList->remove(self::class, $argument); return; } - /** @var File[] $files */ - $files = $uploadFolder->getDirectoryListing(); - // Remove if all files have an mtime of more than a day $time = $this->time->getTime() - 60 * 60 * 24; + if (!($uploadFolder instanceof Folder)) { + $this->logger->error("Found a file inside the uploads folder. Uid: " . $uid . ' folder: ' . $folder); + if ($uploadFolder->getMTime() < $time) { + $uploadFolder->delete(); + } + $this->jobList->remove(self::class, $argument); + return; + } + + /** @var File[] $files */ + $files = $uploadFolder->getDirectoryListing(); + // The folder has to be more than a day old $initial = $uploadFolder->getMTime() < $time; diff --git a/apps/dav/lib/BackgroundJob/UserStatusAutomation.php b/apps/dav/lib/BackgroundJob/UserStatusAutomation.php new file mode 100644 index 00000000000..bbd92d903ee --- /dev/null +++ b/apps/dav/lib/BackgroundJob/UserStatusAutomation.php @@ -0,0 +1,188 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com> + * + * @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\BackgroundJob; + +use OCA\DAV\CalDAV\Schedule\Plugin; +use OCP\AppFramework\Utility\ITimeFactory; +use OCP\BackgroundJob\IJobList; +use OCP\BackgroundJob\TimedJob; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IConfig; +use OCP\IDBConnection; +use OCP\UserStatus\IManager; +use OCP\UserStatus\IUserStatus; +use Psr\Log\LoggerInterface; +use Sabre\VObject\Component\Available; +use Sabre\VObject\Component\VAvailability; +use Sabre\VObject\Reader; +use Sabre\VObject\Recur\RRuleIterator; + +class UserStatusAutomation extends TimedJob { + protected IDBConnection $connection; + protected IJobList $jobList; + protected LoggerInterface $logger; + protected IManager $manager; + protected IConfig $config; + + public function __construct(ITimeFactory $timeFactory, + IDBConnection $connection, + IJobList $jobList, + LoggerInterface $logger, + IManager $manager, + IConfig $config) { + parent::__construct($timeFactory); + $this->connection = $connection; + $this->jobList = $jobList; + $this->logger = $logger; + $this->manager = $manager; + $this->config = $config; + + // Interval 0 might look weird, but the last_checked is always moved + // to the next time we need this and then it's 0 seconds ago. + $this->setInterval(0); + } + + /** + * @inheritDoc + */ + protected function run($argument) { + if (!isset($argument['userId'])) { + $this->jobList->remove(self::class, $argument); + $this->logger->info('Removing invalid ' . self::class . ' background job'); + return; + } + + $userId = $argument['userId']; + $automationEnabled = $this->config->getUserValue($userId, 'dav', 'user_status_automation', 'no') === 'yes'; + if (!$automationEnabled) { + $this->logger->info('Removing ' . self::class . ' background job for user "' . $userId . '" because the setting is disabled'); + $this->jobList->remove(self::class, $argument); + return; + } + + $property = $this->getAvailabilityFromPropertiesTable($userId); + + if (!$property) { + $this->logger->info('Removing ' . self::class . ' background job for user "' . $userId . '" because the user has no availability settings'); + $this->jobList->remove(self::class, $argument); + return; + } + + $isCurrentlyAvailable = false; + $nextPotentialToggles = []; + + $now = new \DateTime('now'); + $lastMidnight = (clone $now)->setTime(0, 0); + + $vObject = Reader::read($property); + foreach ($vObject->getComponents() as $component) { + if ($component->name !== 'VAVAILABILITY') { + continue; + } + /** @var VAvailability $component */ + $availables = $component->getComponents(); + foreach ($availables as $available) { + /** @var Available $available */ + if ($available->name === 'AVAILABLE') { + /** @var \DateTimeInterface $effectiveStart */ + /** @var \DateTimeInterface $effectiveEnd */ + [$effectiveStart, $effectiveEnd] = $available->getEffectiveStartEnd(); + + try { + $it = new RRuleIterator((string) $available->RRULE, $effectiveStart); + $it->fastForward($lastMidnight); + + $startToday = $it->current(); + if ($startToday && $startToday <= $now) { + $duration = $effectiveStart->diff($effectiveEnd); + $endToday = $startToday->add($duration); + if ($endToday > $now) { + // User is currently available + // Also queuing the end time as next status toggle + $isCurrentlyAvailable = true; + $nextPotentialToggles[] = $endToday->getTimestamp(); + } + + // Availability enabling already done for today, + // so jump to the next recurrence to find the next status toggle + $it->next(); + } + + if ($it->current()) { + $nextPotentialToggles[] = $it->current()->getTimestamp(); + } + } catch (\Exception $e) { + $this->logger->error($e->getMessage(), ['exception' => $e]); + } + } + } + } + + $nextAutomaticToggle = min($nextPotentialToggles); + $this->setLastRunToNextToggleTime($userId, $nextAutomaticToggle - 1); + + if ($isCurrentlyAvailable) { + $this->manager->revertUserStatus($userId, IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND); + } else { + // The DND status automation is more important than the "Away - In call" so we also restore that one if it exists. + $this->manager->revertUserStatus($userId, IUserStatus::MESSAGE_CALL, IUserStatus::AWAY); + $this->manager->setUserStatus($userId, IUserStatus::MESSAGE_AVAILABILITY, IUserStatus::DND, true); + } + $this->logger->debug('User status automation ran'); + } + + protected function setLastRunToNextToggleTime(string $userId, int $timestamp): void { + $query = $this->connection->getQueryBuilder(); + + $query->update('jobs') + ->set('last_run', $query->createNamedParameter($timestamp, IQueryBuilder::PARAM_INT)) + ->where($query->expr()->eq('id', $query->createNamedParameter($this->getId(), IQueryBuilder::PARAM_INT))); + $query->executeStatement(); + + $this->logger->debug('Updated user status automation last_run to ' . $timestamp . ' for user ' . $userId); + } + + /** + * @param string $userId + * @return false|string + */ + protected function getAvailabilityFromPropertiesTable(string $userId) { + $propertyPath = 'calendars/' . $userId . '/inbox'; + $propertyName = '{' . Plugin::NS_CALDAV . '}calendar-availability'; + + $query = $this->connection->getQueryBuilder(); + $query->select('propertyvalue') + ->from('properties') + ->where($query->expr()->eq('userid', $query->createNamedParameter($userId))) + ->andWhere($query->expr()->eq('propertypath', $query->createNamedParameter($propertyPath))) + ->where($query->expr()->eq('propertyname', $query->createNamedParameter($propertyName))) + ->setMaxResults(1); + + $result = $query->executeQuery(); + $property = $result->fetchOne(); + $result->closeCursor(); + + return $property; + } +} diff --git a/apps/dav/lib/CalDAV/Activity/Backend.php b/apps/dav/lib/CalDAV/Activity/Backend.php index 84ba50b8c37..781a456b5b3 100644 --- a/apps/dav/lib/CalDAV/Activity/Backend.php +++ b/apps/dav/lib/CalDAV/Activity/Backend.php @@ -34,6 +34,7 @@ use OCP\App\IAppManager; use OCP\IGroup; use OCP\IGroupManager; use OCP\IUser; +use OCP\IUserManager; use OCP\IUserSession; use Sabre\VObject\Reader; @@ -56,11 +57,15 @@ class Backend { /** @var IAppManager */ protected $appManager; - public function __construct(IActivityManager $activityManager, IGroupManager $groupManager, IUserSession $userSession, IAppManager $appManager) { + /** @var IUserManager */ + protected $userManager; + + public function __construct(IActivityManager $activityManager, IGroupManager $groupManager, IUserSession $userSession, IAppManager $appManager, IUserManager $userManager) { $this->activityManager = $activityManager; $this->groupManager = $groupManager; $this->userSession = $userSession; $this->appManager = $appManager; + $this->userManager = $userManager; } /** @@ -119,7 +124,7 @@ class Backend { * @param array $calendarData * @param bool $publishStatus */ - public function onCalendarPublication(array $calendarData, $publishStatus) { + public function onCalendarPublication(array $calendarData, bool $publishStatus): void { $this->triggerCalendarActivity($publishStatus ? Calendar::SUBJECT_PUBLISH : Calendar::SUBJECT_UNPUBLISH, $calendarData); } @@ -165,6 +170,11 @@ class Backend { } foreach ($users as $user) { + if ($action === Calendar::SUBJECT_DELETE && !$this->userManager->userExists($user)) { + // Avoid creating calendar_delete activities for deleted users + continue; + } + $event->setAffectedUser($user) ->setSubject( $user === $currentUser ? $action . '_self' : $action, @@ -438,6 +448,11 @@ class Backend { $classification = $objectData['classification'] ?? CalDavBackend::CLASSIFICATION_PUBLIC; $object = $this->getObjectNameAndType($objectData); + + if (!$object) { + return; + } + $action = $action . '_' . $object['type']; if ($object['type'] === 'todo' && strpos($action, Event::SUBJECT_OBJECT_UPDATE) === 0 && $object['status'] === 'COMPLETED') { @@ -494,8 +509,103 @@ class Backend { } /** + * Creates activities when a calendar object was moved + */ + public function onMovedCalendarObject(array $sourceCalendarData, array $targetCalendarData, array $sourceShares, array $targetShares, array $objectData): void { + if (!isset($targetCalendarData['principaluri'])) { + return; + } + + $sourcePrincipal = explode('/', $sourceCalendarData['principaluri']); + $sourceOwner = array_pop($sourcePrincipal); + + $targetPrincipal = explode('/', $targetCalendarData['principaluri']); + $targetOwner = array_pop($targetPrincipal); + + if ($sourceOwner !== $targetOwner) { + $this->onTouchCalendarObject( + Event::SUBJECT_OBJECT_DELETE, + $sourceCalendarData, + $sourceShares, + $objectData + ); + $this->onTouchCalendarObject( + Event::SUBJECT_OBJECT_ADD, + $targetCalendarData, + $targetShares, + $objectData + ); + return; + } + + $currentUser = $this->userSession->getUser(); + if ($currentUser instanceof IUser) { + $currentUser = $currentUser->getUID(); + } else { + $currentUser = $targetOwner; + } + + $classification = $objectData['classification'] ?? CalDavBackend::CLASSIFICATION_PUBLIC; + $object = $this->getObjectNameAndType($objectData); + + if (!$object) { + return; + } + + $event = $this->activityManager->generateEvent(); + $event->setApp('dav') + ->setObject('calendar', (int) $targetCalendarData['id']) + ->setType($object['type'] === 'event' ? 'calendar_event' : 'calendar_todo') + ->setAuthor($currentUser); + + $users = $this->getUsersForShares(array_intersect($sourceShares, $targetShares)); + $users[] = $targetOwner; + + // Users for share can return the owner itself if the calendar is published + foreach (array_unique($users) as $user) { + if ($classification === CalDavBackend::CLASSIFICATION_PRIVATE && $user !== $targetOwner) { + // Private events are only shown to the owner + continue; + } + + $params = [ + 'actor' => $event->getAuthor(), + 'sourceCalendar' => [ + 'id' => (int) $sourceCalendarData['id'], + 'uri' => $sourceCalendarData['uri'], + 'name' => $sourceCalendarData['{DAV:}displayname'], + ], + 'targetCalendar' => [ + 'id' => (int) $targetCalendarData['id'], + 'uri' => $targetCalendarData['uri'], + 'name' => $targetCalendarData['{DAV:}displayname'], + ], + 'object' => [ + 'id' => $object['id'], + 'name' => $classification === CalDavBackend::CLASSIFICATION_CONFIDENTIAL && $user !== $targetOwner ? 'Busy' : $object['name'], + 'classified' => $classification === CalDavBackend::CLASSIFICATION_CONFIDENTIAL && $user !== $targetOwner, + ], + ]; + + if ($object['type'] === 'event' && $this->appManager->isEnabledForUser('calendar')) { + $params['object']['link']['object_uri'] = $objectData['uri']; + $params['object']['link']['calendar_uri'] = $targetCalendarData['uri']; + $params['object']['link']['owner'] = $targetOwner; + } + + $event->setAffectedUser($user) + ->setSubject( + $user === $currentUser ? Event::SUBJECT_OBJECT_MOVE . '_' . $object['type'] . '_self' : Event::SUBJECT_OBJECT_MOVE . '_' . $object['type'], + $params + ); + + $this->activityManager->publish($event); + } + } + + /** * @param array $objectData - * @return string[]|bool + * @return string[]|false */ protected function getObjectNameAndType(array $objectData) { $vObject = Reader::read($objectData['calendardata']); diff --git a/apps/dav/lib/CalDAV/Activity/Filter/Todo.php b/apps/dav/lib/CalDAV/Activity/Filter/Todo.php index f727c10befe..6f1dbbdcb5c 100644 --- a/apps/dav/lib/CalDAV/Activity/Filter/Todo.php +++ b/apps/dav/lib/CalDAV/Activity/Filter/Todo.php @@ -52,7 +52,7 @@ class Todo implements IFilter { * @since 11.0.0 */ public function getName() { - return $this->l->t('Todos'); + return $this->l->t('To-dos'); } /** diff --git a/apps/dav/lib/CalDAV/Activity/Provider/Event.php b/apps/dav/lib/CalDAV/Activity/Provider/Event.php index 3ed591219af..9ae04aadbba 100644 --- a/apps/dav/lib/CalDAV/Activity/Provider/Event.php +++ b/apps/dav/lib/CalDAV/Activity/Provider/Event.php @@ -40,6 +40,7 @@ use OCP\L10N\IFactory; class Event extends Base { public const SUBJECT_OBJECT_ADD = 'object_add'; public const SUBJECT_OBJECT_UPDATE = 'object_update'; + public const SUBJECT_OBJECT_MOVE = 'object_move'; public const SUBJECT_OBJECT_MOVE_TO_TRASH = 'object_move_to_trash'; public const SUBJECT_OBJECT_RESTORE = 'object_restore'; public const SUBJECT_OBJECT_DELETE = 'object_delete'; @@ -145,6 +146,10 @@ class Event extends Base { $subject = $this->l->t('{actor} updated event {event} in calendar {calendar}'); } elseif ($event->getSubject() === self::SUBJECT_OBJECT_UPDATE . '_event_self') { $subject = $this->l->t('You updated event {event} in calendar {calendar}'); + } elseif ($event->getSubject() === self::SUBJECT_OBJECT_MOVE . '_event') { + $subject = $this->l->t('{actor} moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}'); + } elseif ($event->getSubject() === self::SUBJECT_OBJECT_MOVE . '_event_self') { + $subject = $this->l->t('You moved event {event} from calendar {sourceCalendar} to calendar {targetCalendar}'); } elseif ($event->getSubject() === self::SUBJECT_OBJECT_MOVE_TO_TRASH . '_event') { $subject = $this->l->t('{actor} deleted event {event} from calendar {calendar}'); } elseif ($event->getSubject() === self::SUBJECT_OBJECT_MOVE_TO_TRASH . '_event_self') { @@ -198,6 +203,24 @@ class Event extends Base { } } + if (isset($parameters['sourceCalendar']) && isset($parameters['targetCalendar'])) { + switch ($subject) { + case self::SUBJECT_OBJECT_MOVE . '_event': + return [ + 'actor' => $this->generateUserParameter($parameters['actor']), + 'sourceCalendar' => $this->generateCalendarParameter($parameters['sourceCalendar'], $this->l), + 'targetCalendar' => $this->generateCalendarParameter($parameters['targetCalendar'], $this->l), + 'event' => $this->generateClassifiedObjectParameter($parameters['object']), + ]; + case self::SUBJECT_OBJECT_MOVE . '_event_self': + return [ + 'sourceCalendar' => $this->generateCalendarParameter($parameters['sourceCalendar'], $this->l), + 'targetCalendar' => $this->generateCalendarParameter($parameters['targetCalendar'], $this->l), + 'event' => $this->generateClassifiedObjectParameter($parameters['object']), + ]; + } + } + // Legacy - Do NOT Remove unless necessary // Removing this will break parsing of activities that were created on // Nextcloud 12, so we should keep this as long as it's acceptable. diff --git a/apps/dav/lib/CalDAV/Activity/Provider/Todo.php b/apps/dav/lib/CalDAV/Activity/Provider/Todo.php index a3ab81e38ae..e2ddb99a1af 100644 --- a/apps/dav/lib/CalDAV/Activity/Provider/Todo.php +++ b/apps/dav/lib/CalDAV/Activity/Provider/Todo.php @@ -50,25 +50,29 @@ class Todo extends Event { } if ($event->getSubject() === self::SUBJECT_OBJECT_ADD . '_todo') { - $subject = $this->l->t('{actor} created todo {todo} in list {calendar}'); + $subject = $this->l->t('{actor} created to-do {todo} in list {calendar}'); } elseif ($event->getSubject() === self::SUBJECT_OBJECT_ADD . '_todo_self') { - $subject = $this->l->t('You created todo {todo} in list {calendar}'); + $subject = $this->l->t('You created to-do {todo} in list {calendar}'); } elseif ($event->getSubject() === self::SUBJECT_OBJECT_DELETE . '_todo') { - $subject = $this->l->t('{actor} deleted todo {todo} from list {calendar}'); + $subject = $this->l->t('{actor} deleted to-do {todo} from list {calendar}'); } elseif ($event->getSubject() === self::SUBJECT_OBJECT_DELETE . '_todo_self') { - $subject = $this->l->t('You deleted todo {todo} from list {calendar}'); + $subject = $this->l->t('You deleted to-do {todo} from list {calendar}'); } elseif ($event->getSubject() === self::SUBJECT_OBJECT_UPDATE . '_todo') { - $subject = $this->l->t('{actor} updated todo {todo} in list {calendar}'); + $subject = $this->l->t('{actor} updated to-do {todo} in list {calendar}'); } elseif ($event->getSubject() === self::SUBJECT_OBJECT_UPDATE . '_todo_self') { - $subject = $this->l->t('You updated todo {todo} in list {calendar}'); + $subject = $this->l->t('You updated to-do {todo} in list {calendar}'); } elseif ($event->getSubject() === self::SUBJECT_OBJECT_UPDATE . '_todo_completed') { - $subject = $this->l->t('{actor} solved todo {todo} in list {calendar}'); + $subject = $this->l->t('{actor} solved to-do {todo} in list {calendar}'); } elseif ($event->getSubject() === self::SUBJECT_OBJECT_UPDATE . '_todo_completed_self') { - $subject = $this->l->t('You solved todo {todo} in list {calendar}'); + $subject = $this->l->t('You solved to-do {todo} in list {calendar}'); } elseif ($event->getSubject() === self::SUBJECT_OBJECT_UPDATE . '_todo_needs_action') { - $subject = $this->l->t('{actor} reopened todo {todo} in list {calendar}'); + $subject = $this->l->t('{actor} reopened to-do {todo} in list {calendar}'); } elseif ($event->getSubject() === self::SUBJECT_OBJECT_UPDATE . '_todo_needs_action_self') { - $subject = $this->l->t('You reopened todo {todo} in list {calendar}'); + $subject = $this->l->t('You reopened to-do {todo} in list {calendar}'); + } elseif ($event->getSubject() === self::SUBJECT_OBJECT_MOVE . '_todo') { + $subject = $this->l->t('{actor} moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}'); + } elseif ($event->getSubject() === self::SUBJECT_OBJECT_MOVE . '_todo_self') { + $subject = $this->l->t('You moved to-do {todo} from list {sourceCalendar} to list {targetCalendar}'); } else { throw new \InvalidArgumentException(); } @@ -114,6 +118,24 @@ class Todo extends Event { } } + if (isset($parameters['sourceCalendar']) && isset($parameters['targetCalendar'])) { + switch ($subject) { + case self::SUBJECT_OBJECT_MOVE . '_todo': + return [ + 'actor' => $this->generateUserParameter($parameters['actor']), + 'sourceCalendar' => $this->generateCalendarParameter($parameters['sourceCalendar'], $this->l), + 'targetCalendar' => $this->generateCalendarParameter($parameters['targetCalendar'], $this->l), + 'todo' => $this->generateObjectParameter($parameters['object']), + ]; + case self::SUBJECT_OBJECT_MOVE . '_todo_self': + return [ + 'sourceCalendar' => $this->generateCalendarParameter($parameters['sourceCalendar'], $this->l), + 'targetCalendar' => $this->generateCalendarParameter($parameters['targetCalendar'], $this->l), + 'todo' => $this->generateObjectParameter($parameters['object']), + ]; + } + } + // Legacy - Do NOT Remove unless necessary // Removing this will break parsing of activities that were created on // Nextcloud 12, so we should keep this as long as it's acceptable. diff --git a/apps/dav/lib/CalDAV/Activity/Setting/Todo.php b/apps/dav/lib/CalDAV/Activity/Setting/Todo.php index 7d27b30c4af..a4ac3f5d569 100644 --- a/apps/dav/lib/CalDAV/Activity/Setting/Todo.php +++ b/apps/dav/lib/CalDAV/Activity/Setting/Todo.php @@ -38,7 +38,7 @@ class Todo extends CalDAVSetting { * @since 11.0.0 */ public function getName() { - return $this->l->t('A calendar <strong>todo</strong> was modified'); + return $this->l->t('A calendar <strong>to-do</strong> was modified'); } /** diff --git a/apps/dav/lib/CalDAV/BirthdayService.php b/apps/dav/lib/CalDAV/BirthdayService.php index 1030768e26f..be6b0911538 100644 --- a/apps/dav/lib/CalDAV/BirthdayService.php +++ b/apps/dav/lib/CalDAV/BirthdayService.php @@ -14,6 +14,7 @@ declare(strict_types=1); * @author Sven Strickroth <email@cs-ware.de> * @author Thomas Müller <thomas.mueller@tmit.eu> * @author Valdnet <47037905+Valdnet@users.noreply.github.com> + * @author Cédric Neukom <github@webguy.ch> * * @license AGPL-3.0 * @@ -86,6 +87,9 @@ class BirthdayService { $targetPrincipals = $this->getAllAffectedPrincipals($addressBookId); $book = $this->cardDavBackEnd->getAddressBookById($addressBookId); + if ($book === null) { + return; + } $targetPrincipals[] = $book['principaluri']; $datesToSync = [ ['postfix' => '', 'field' => 'BDAY'], @@ -98,9 +102,14 @@ class BirthdayService { continue; } + $reminderOffset = $this->getReminderOffsetForUser($principalUri); + $calendar = $this->ensureCalendarExists($principalUri); + if ($calendar === null) { + return; + } foreach ($datesToSync as $type) { - $this->updateCalendar($cardUri, $cardData, $book, (int) $calendar['id'], $type); + $this->updateCalendar($cardUri, $cardData, $book, (int) $calendar['id'], $type, $reminderOffset); } } } @@ -148,12 +157,14 @@ class BirthdayService { * @param $cardData * @param $dateField * @param $postfix + * @param $reminderOffset * @return VCalendar|null * @throws InvalidDataException */ - public function buildDateFromContact(string $cardData, - string $dateField, - string $postfix):?VCalendar { + public function buildDateFromContact(string $cardData, + string $dateField, + string $postfix, + ?string $reminderOffset):?VCalendar { if (empty($cardData)) { return null; } @@ -258,11 +269,13 @@ class BirthdayService { if ($originalYear !== null) { $vEvent->{'X-NEXTCLOUD-BC-YEAR'} = (string) $originalYear; } - $alarm = $vCal->createComponent('VALARM'); - $alarm->add($vCal->createProperty('TRIGGER', '-PT0M', ['VALUE' => 'DURATION'])); - $alarm->add($vCal->createProperty('ACTION', 'DISPLAY')); - $alarm->add($vCal->createProperty('DESCRIPTION', $vEvent->{'SUMMARY'})); - $vEvent->add($alarm); + if ($reminderOffset) { + $alarm = $vCal->createComponent('VALARM'); + $alarm->add($vCal->createProperty('TRIGGER', $reminderOffset, ['VALUE' => 'DURATION'])); + $alarm->add($vCal->createProperty('ACTION', 'DISPLAY')); + $alarm->add($vCal->createProperty('DESCRIPTION', $vEvent->{'SUMMARY'})); + $vEvent->add($alarm); + } $vCal->add($vEvent); return $vCal; } @@ -273,6 +286,9 @@ class BirthdayService { public function resetForUser(string $user):void { $principal = 'principals/users/'.$user; $calendar = $this->calDavBackEnd->getCalendarByUri($principal, self::BIRTHDAY_CALENDAR_URI); + if (!$calendar) { + return; // The user's birthday calendar doesn't exist, no need to purge it + } $calendarObjects = $this->calDavBackEnd->getCalendarObjects($calendar['id'], CalDavBackend::CALENDAR_TYPE_CALENDAR); foreach ($calendarObjects as $calendarObject) { @@ -341,6 +357,7 @@ class BirthdayService { * @param array $book * @param int $calendarId * @param array $type + * @param string $reminderOffset * @throws InvalidDataException * @throws \Sabre\DAV\Exception\BadRequest */ @@ -348,9 +365,10 @@ class BirthdayService { string $cardData, array $book, int $calendarId, - array $type):void { + array $type, + ?string $reminderOffset):void { $objectUri = $book['uri'] . '-' . $cardUri . $type['postfix'] . '.ics'; - $calendarData = $this->buildDateFromContact($cardData, $type['field'], $type['postfix']); + $calendarData = $this->buildDateFromContact($cardData, $type['field'], $type['postfix'], $reminderOffset); $existing = $this->calDavBackEnd->getCalendarObject($calendarId, $objectUri); if ($calendarData === null) { if ($existing !== null) { @@ -391,14 +409,27 @@ class BirthdayService { } /** + * Extracts the userId part of a principal + * + * @param string $userPrincipal + * @return string|null + */ + private function principalToUserId(string $userPrincipal):?string { + if (substr($userPrincipal, 0, 17) === 'principals/users/') { + return substr($userPrincipal, 17); + } + return null; + } + + /** * Checks if the user opted-out of birthday calendars * * @param string $userPrincipal The user principal to check for * @return bool */ private function isUserEnabled(string $userPrincipal):bool { - if (strpos($userPrincipal, 'principals/users/') === 0) { - $userId = substr($userPrincipal, 17); + $userId = $this->principalToUserId($userPrincipal); + if ($userId !== null) { $isEnabled = $this->config->getUserValue($userId, 'dav', 'generateBirthdayCalendar', 'yes'); return $isEnabled === 'yes'; } @@ -408,6 +439,23 @@ class BirthdayService { } /** + * Get the reminder offset value for a user. This is a duration string (e.g. + * PT9H) or null if no reminder is wanted. + * + * @param string $userPrincipal + * @return string|null + */ + private function getReminderOffsetForUser(string $userPrincipal):?string { + $userId = $this->principalToUserId($userPrincipal); + if ($userId !== null) { + return $this->config->getUserValue($userId, 'dav', 'birthdayCalendarReminderOffset', 'PT9H') ?: null; + } + + // not sure how we got here, just be on the safe side and return the default value + return 'PT9H'; + } + + /** * Formats title of Birthday event * * @param string $field Field name like BDAY, ANNIVERSARY, ... diff --git a/apps/dav/lib/CalDAV/CalDavBackend.php b/apps/dav/lib/CalDAV/CalDavBackend.php index 8bdc084cd7b..2bbdc51f42e 100644 --- a/apps/dav/lib/CalDAV/CalDavBackend.php +++ b/apps/dav/lib/CalDAV/CalDavBackend.php @@ -52,6 +52,7 @@ use OCA\DAV\Events\CalendarDeletedEvent; use OCA\DAV\Events\CalendarMovedToTrashEvent; use OCA\DAV\Events\CalendarObjectCreatedEvent; use OCA\DAV\Events\CalendarObjectDeletedEvent; +use OCA\DAV\Events\CalendarObjectMovedEvent; use OCA\DAV\Events\CalendarObjectMovedToTrashEvent; use OCA\DAV\Events\CalendarObjectRestoredEvent; use OCA\DAV\Events\CalendarObjectUpdatedEvent; @@ -95,8 +96,6 @@ use Sabre\VObject\ParseException; use Sabre\VObject\Property; use Sabre\VObject\Reader; use Sabre\VObject\Recur\EventIterator; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Symfony\Component\EventDispatcher\GenericEvent; use function array_column; use function array_merge; use function array_values; @@ -150,7 +149,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription * @var array * @psalm-var array<string, string[]> */ - public $propertyMap = [ + public array $propertyMap = [ '{DAV:}displayname' => ['displayname', 'string'], '{urn:ietf:params:xml:ns:caldav}calendar-description' => ['description', 'string'], '{urn:ietf:params:xml:ns:caldav}calendar-timezone' => ['timezone', 'string'], @@ -164,7 +163,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription * * @var array */ - public $subscriptionPropertyMap = [ + public array $subscriptionPropertyMap = [ '{DAV:}displayname' => ['displayname', 'string'], '{http://apple.com/ns/ical/}refreshrate' => ['refreshrate', 'string'], '{http://apple.com/ns/ical/}calendar-order' => ['calendarorder', 'int'], @@ -195,7 +194,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription ]; /** @var array parameters to index */ - public static $indexParameters = [ + public static array $indexParameters = [ 'ATTENDEE' => ['CN'], 'ORGANIZER' => ['CN'], ]; @@ -203,43 +202,19 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription /** * @var string[] Map of uid => display name */ - protected $userDisplayNames; - - /** @var IDBConnection */ - private $db; - - /** @var Backend */ - private $calendarSharingBackend; - - /** @var Principal */ - private $principalBackend; - - /** @var IUserManager */ - private $userManager; - - /** @var ISecureRandom */ - private $random; + protected array $userDisplayNames; + private IDBConnection $db; + private Backend $calendarSharingBackend; + private Principal $principalBackend; + private IUserManager $userManager; + private ISecureRandom $random; private LoggerInterface $logger; + private IEventDispatcher $dispatcher; + private IConfig $config; + private bool $legacyEndpoint; + private string $dbObjectPropertiesTable = 'calendarobjects_props'; - /** @var IEventDispatcher */ - private $dispatcher; - - /** @var EventDispatcherInterface */ - private $legacyDispatcher; - - /** @var IConfig */ - private $config; - - /** @var bool */ - private $legacyEndpoint; - - /** @var string */ - private $dbObjectPropertiesTable = 'calendarobjects_props'; - - /** - * CalDavBackend constructor. - */ public function __construct(IDBConnection $db, Principal $principalBackend, IUserManager $userManager, @@ -247,7 +222,6 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription ISecureRandom $random, LoggerInterface $logger, IEventDispatcher $dispatcher, - EventDispatcherInterface $legacyDispatcher, IConfig $config, bool $legacyEndpoint = false) { $this->db = $db; @@ -257,7 +231,6 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $this->random = $random; $this->logger = $logger; $this->dispatcher = $dispatcher; - $this->legacyDispatcher = $legacyDispatcher; $this->config = $config; $this->legacyEndpoint = $legacyEndpoint; } @@ -701,10 +674,9 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription } /** - * @param $calendarId - * @return array|null + * @return array{id: int, uri: string, '{http://calendarserver.org/ns/}getctag': string, '{http://sabredav.org/ns}sync-token': int, '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set': SupportedCalendarComponentSet, '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp': ScheduleCalendarTransp }|null */ - public function getCalendarById($calendarId) { + public function getCalendarById(int $calendarId): ?array { $fields = array_column($this->propertyMap, 0); $fields[] = 'id'; $fields[] = 'uri'; @@ -737,7 +709,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription 'uri' => $row['uri'], 'principaluri' => $this->convertPrincipal($row['principaluri'], !$this->legacyEndpoint), '{' . Plugin::NS_CALENDARSERVER . '}getctag' => 'http://sabre.io/ns/sync/' . ($row['synctoken']?$row['synctoken']:'0'), - '{http://sabredav.org/ns}sync-token' => $row['synctoken']?$row['synctoken']:'0', + '{http://sabredav.org/ns}sync-token' => $row['synctoken'] ?? 0, '{' . Plugin::NS_CALDAV . '}supported-calendar-component-set' => new SupportedCalendarComponentSet($components), '{' . Plugin::NS_CALDAV . '}schedule-calendar-transp' => new ScheduleCalendarTransp($row['transparent']?'transparent':'opaque'), ]; @@ -893,7 +865,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $calendarData = $this->getCalendarById($calendarId); $shares = $this->getShares($calendarId); - $this->dispatcher->dispatchTyped(new CalendarUpdatedEvent((int)$calendarId, $calendarData, $shares, $mutations)); + $this->dispatcher->dispatchTyped(new CalendarUpdatedEvent($calendarId, $calendarData, $shares, $mutations)); return true; }); @@ -943,7 +915,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription // Only dispatch if we actually deleted anything if ($calendarData) { - $this->dispatcher->dispatchTyped(new CalendarDeletedEvent((int)$calendarId, $calendarData, $shares)); + $this->dispatcher->dispatchTyped(new CalendarDeletedEvent($calendarId, $calendarData, $shares)); } } else { $qbMarkCalendarDeleted = $this->db->getQueryBuilder(); @@ -956,7 +928,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $shares = $this->getShares($calendarId); if ($calendarData) { $this->dispatcher->dispatchTyped(new CalendarMovedToTrashEvent( - (int)$calendarId, + $calendarId, $calendarData, $shares )); @@ -1292,24 +1264,17 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $this->addChange($calendarId, $objectUri, 1, $calendarType); $objectRow = $this->getCalendarObject($calendarId, $objectUri, $calendarType); + assert($objectRow !== null); + if ($calendarType === self::CALENDAR_TYPE_CALENDAR) { $calendarRow = $this->getCalendarById($calendarId); $shares = $this->getShares($calendarId); - $this->dispatcher->dispatchTyped(new CalendarObjectCreatedEvent((int)$calendarId, $calendarRow, $shares, $objectRow)); + $this->dispatcher->dispatchTyped(new CalendarObjectCreatedEvent($calendarId, $calendarRow, $shares, $objectRow)); } else { $subscriptionRow = $this->getSubscriptionById($calendarId); - $this->dispatcher->dispatchTyped(new CachedCalendarObjectCreatedEvent((int)$calendarId, $subscriptionRow, [], $objectRow)); - $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createCachedCalendarObject', new GenericEvent( - '\OCA\DAV\CalDAV\CalDavBackend::createCachedCalendarObject', - [ - 'subscriptionId' => $calendarId, - 'calendarData' => $subscriptionRow, - 'shares' => [], - 'objectData' => $objectRow, - ] - )); + $this->dispatcher->dispatchTyped(new CachedCalendarObjectCreatedEvent($calendarId, $subscriptionRow, [], $objectRow)); } return '"' . $extraData['etag'] . '"'; @@ -1361,20 +1326,11 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $calendarRow = $this->getCalendarById($calendarId); $shares = $this->getShares($calendarId); - $this->dispatcher->dispatchTyped(new CalendarObjectUpdatedEvent((int)$calendarId, $calendarRow, $shares, $objectRow)); + $this->dispatcher->dispatchTyped(new CalendarObjectUpdatedEvent($calendarId, $calendarRow, $shares, $objectRow)); } else { $subscriptionRow = $this->getSubscriptionById($calendarId); - $this->dispatcher->dispatchTyped(new CachedCalendarObjectUpdatedEvent((int)$calendarId, $subscriptionRow, [], $objectRow)); - $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateCachedCalendarObject', new GenericEvent( - '\OCA\DAV\CalDAV\CalDavBackend::updateCachedCalendarObject', - [ - 'subscriptionId' => $calendarId, - 'calendarData' => $subscriptionRow, - 'shares' => [], - 'objectData' => $objectRow, - ] - )); + $this->dispatcher->dispatchTyped(new CachedCalendarObjectUpdatedEvent($calendarId, $subscriptionRow, [], $objectRow)); } } @@ -1387,13 +1343,14 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription * @param int $sourceCalendarId * @param int $targetCalendarId * @param int $objectId - * @param string $principalUri + * @param string $oldPrincipalUri + * @param string $newPrincipalUri * @param int $calendarType * @return bool * @throws Exception */ - public function moveCalendarObject(int $sourceCalendarId, int $targetCalendarId, int $objectId, string $principalUri, int $calendarType = self::CALENDAR_TYPE_CALENDAR): bool { - $object = $this->getCalendarObjectById($principalUri, $objectId); + public function moveCalendarObject(int $sourceCalendarId, int $targetCalendarId, int $objectId, string $oldPrincipalUri, string $newPrincipalUri, int $calendarType = self::CALENDAR_TYPE_CALENDAR): bool { + $object = $this->getCalendarObjectById($oldPrincipalUri, $objectId); if (empty($object)) { return false; } @@ -1411,21 +1368,23 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $this->addChange($sourceCalendarId, $object['uri'], 1, $calendarType); $this->addChange($targetCalendarId, $object['uri'], 3, $calendarType); - $object = $this->getCalendarObjectById($principalUri, $objectId); + $object = $this->getCalendarObjectById($newPrincipalUri, $objectId); // Calendar Object wasn't found - possibly because it was deleted in the meantime by a different client if (empty($object)) { return false; } - $calendarRow = $this->getCalendarById($targetCalendarId); + $targetCalendarRow = $this->getCalendarById($targetCalendarId); // the calendar this event is being moved to does not exist any longer - if (empty($calendarRow)) { + if (empty($targetCalendarRow)) { return false; } if ($calendarType === self::CALENDAR_TYPE_CALENDAR) { - $shares = $this->getShares($targetCalendarId); - $this->dispatcher->dispatchTyped(new CalendarObjectUpdatedEvent($targetCalendarId, $calendarRow, $shares, $object)); + $sourceShares = $this->getShares($sourceCalendarId); + $targetShares = $this->getShares($targetCalendarId); + $sourceCalendarRow = $this->getCalendarById($sourceCalendarId); + $this->dispatcher->dispatchTyped(new CalendarObjectMovedEvent($sourceCalendarId, $sourceCalendarRow, $targetCalendarId, $targetCalendarRow, $sourceShares, $targetShares, $object)); } return true; } @@ -1477,20 +1436,11 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $calendarRow = $this->getCalendarById($calendarId); $shares = $this->getShares($calendarId); - $this->dispatcher->dispatchTyped(new CalendarObjectDeletedEvent((int)$calendarId, $calendarRow, $shares, $data)); + $this->dispatcher->dispatchTyped(new CalendarObjectDeletedEvent($calendarId, $calendarRow, $shares, $data)); } else { $subscriptionRow = $this->getSubscriptionById($calendarId); - $this->dispatcher->dispatchTyped(new CachedCalendarObjectDeletedEvent((int)$calendarId, $subscriptionRow, [], $data)); - $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteCachedCalendarObject', new GenericEvent( - '\OCA\DAV\CalDAV\CalDavBackend::deleteCachedCalendarObject', - [ - 'subscriptionId' => $calendarId, - 'calendarData' => $subscriptionRow, - 'shares' => [], - 'objectData' => $data, - ] - )); + $this->dispatcher->dispatchTyped(new CachedCalendarObjectDeletedEvent($calendarId, $subscriptionRow, [], $data)); } } else { $pathInfo = pathinfo($data['uri']); @@ -1530,7 +1480,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription if ($calendarData !== null) { $this->dispatcher->dispatchTyped( new CalendarObjectMovedToTrashEvent( - (int)$calendarId, + $calendarId, $calendarData, $this->getShares($calendarId), $data @@ -1634,7 +1584,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription * Note that especially time-range-filters may be difficult to parse. A * time-range filter specified on a VEVENT must for instance also handle * recurrence rules correctly. - * A good example of how to interprete all these filters can also simply + * A good example of how to interpret all these filters can also simply * be found in Sabre\CalDAV\CalendarQueryFilter. This class is as correct * as possible, so it gives you a good idea on what type of stuff you need * to think of. @@ -2501,12 +2451,6 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $subscriptionRow = $this->getSubscriptionById($subscriptionId); $this->dispatcher->dispatchTyped(new SubscriptionCreatedEvent($subscriptionId, $subscriptionRow)); - $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::createSubscription', new GenericEvent( - '\OCA\DAV\CalDAV\CalDavBackend::createSubscription', - [ - 'subscriptionId' => $subscriptionId, - 'subscriptionData' => $subscriptionRow, - ])); return $subscriptionId; } @@ -2554,13 +2498,6 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription $subscriptionRow = $this->getSubscriptionById($subscriptionId); $this->dispatcher->dispatchTyped(new SubscriptionUpdatedEvent((int)$subscriptionId, $subscriptionRow, [], $mutations)); - $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateSubscription', new GenericEvent( - '\OCA\DAV\CalDAV\CalDavBackend::updateSubscription', - [ - 'subscriptionId' => $subscriptionId, - 'subscriptionData' => $subscriptionRow, - 'propertyMutations' => $mutations, - ])); return true; }); @@ -2575,13 +2512,6 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription public function deleteSubscription($subscriptionId) { $subscriptionRow = $this->getSubscriptionById($subscriptionId); - $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::deleteSubscription', new GenericEvent( - '\OCA\DAV\CalDAV\CalDavBackend::deleteSubscription', - [ - 'subscriptionId' => $subscriptionId, - 'subscriptionData' => $this->getSubscriptionById($subscriptionId), - ])); - $query = $this->db->getQueryBuilder(); $query->delete('calendarsubscriptions') ->where($query->expr()->eq('id', $query->createNamedParameter($subscriptionId))) @@ -2870,34 +2800,26 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription } /** - * @param IShareable $shareable - * @param array $add - * @param array $remove + * @param list<array{href: string, commonName: string, readOnly: bool}> $add + * @param list<string> $remove */ - public function updateShares($shareable, $add, $remove) { + public function updateShares(IShareable $shareable, array $add, array $remove): void { $calendarId = $shareable->getResourceId(); $calendarRow = $this->getCalendarById($calendarId); + if ($calendarRow === null) { + throw new \RuntimeException('Trying to update shares for innexistant calendar: ' . $calendarId); + } $oldShares = $this->getShares($calendarId); - $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::updateShares', new GenericEvent( - '\OCA\DAV\CalDAV\CalDavBackend::updateShares', - [ - 'calendarId' => $calendarId, - 'calendarData' => $calendarRow, - 'shares' => $oldShares, - 'add' => $add, - 'remove' => $remove, - ])); $this->calendarSharingBackend->updateShares($shareable, $add, $remove); - $this->dispatcher->dispatchTyped(new CalendarShareUpdatedEvent((int)$calendarId, $calendarRow, $oldShares, $add, $remove)); + $this->dispatcher->dispatchTyped(new CalendarShareUpdatedEvent($calendarId, $calendarRow, $oldShares, $add, $remove)); } /** - * @param int $resourceId - * @return array + * @return list<array{href: string, commonName: string, status: int, readOnly: bool, '{http://owncloud.org/ns}principal': string, '{http://owncloud.org/ns}group-share': bool}> */ - public function getShares($resourceId) { + public function getShares(int $resourceId): array { return $this->calendarSharingBackend->getShares($resourceId); } @@ -2909,13 +2831,6 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription public function setPublishStatus($value, $calendar) { $calendarId = $calendar->getResourceId(); $calendarData = $this->getCalendarById($calendarId); - $this->legacyDispatcher->dispatch('\OCA\DAV\CalDAV\CalDavBackend::publishCalendar', new GenericEvent( - '\OCA\DAV\CalDAV\CalDavBackend::updateShares', - [ - 'calendarId' => $calendarId, - 'calendarData' => $calendarData, - 'public' => $value, - ])); $query = $this->db->getQueryBuilder(); if ($value) { @@ -2930,7 +2845,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription ]); $query->executeStatement(); - $this->dispatcher->dispatchTyped(new CalendarPublishedEvent((int)$calendarId, $calendarData, $publicUri)); + $this->dispatcher->dispatchTyped(new CalendarPublishedEvent($calendarId, $calendarData, $publicUri)); return $publicUri; } $query->delete('dav_shares') @@ -2938,7 +2853,7 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription ->andWhere($query->expr()->eq('access', $query->createNamedParameter(self::ACCESS_PUBLIC))); $query->executeStatement(); - $this->dispatcher->dispatchTyped(new CalendarUnpublishedEvent((int)$calendarId, $calendarData)); + $this->dispatcher->dispatchTyped(new CalendarUnpublishedEvent($calendarId, $calendarData)); return null; } @@ -2961,15 +2876,13 @@ class CalDavBackend extends AbstractBackend implements SyncSupport, Subscription /** * @param int $resourceId - * @param array $acl - * @return array + * @param list<array{privilege: string, principal: string, protected: bool}> $acl + * @return list<array{privilege: string, principal: string, protected: bool}> */ - public function applyShareAcl($resourceId, $acl) { + public function applyShareAcl(int $resourceId, array $acl): array { return $this->calendarSharingBackend->applyShareAcl($resourceId, $acl); } - - /** * update properties table * diff --git a/apps/dav/lib/CalDAV/Calendar.php b/apps/dav/lib/CalDAV/Calendar.php index 75c815c3b0a..92ad3242d78 100644 --- a/apps/dav/lib/CalDAV/Calendar.php +++ b/apps/dav/lib/CalDAV/Calendar.php @@ -51,27 +51,11 @@ use Sabre\DAV\PropPatch; * @property CalDavBackend $caldavBackend */ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable, IMoveTarget { + private IConfig $config; + protected IL10N $l10n; + private bool $useTrashbin = true; + private LoggerInterface $logger; - /** @var IConfig */ - private $config; - - /** @var IL10N */ - protected $l10n; - - /** @var bool */ - private $useTrashbin = true; - - /** @var LoggerInterface */ - private $logger; - - /** - * Calendar constructor. - * - * @param BackendInterface $caldavBackend - * @param $calendarInfo - * @param IL10N $l10n - * @param IConfig $config - */ public function __construct(BackendInterface $caldavBackend, $calendarInfo, IL10N $l10n, IConfig $config, LoggerInterface $logger) { // Convert deletion date to ISO8601 string if (isset($calendarInfo[TrashbinPlugin::PROPERTY_DELETED_AT])) { @@ -96,25 +80,10 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable } /** - * Updates the list of shares. - * - * The first array is a list of people that are to be added to the - * resource. - * - * Every element in the add array has the following properties: - * * href - A url. Usually a mailto: address - * * commonName - Usually a first and last name, or false - * * summary - A description of the share, can also be false - * * readOnly - A boolean value - * - * Every element in the remove array is just the address string. - * - * @param array $add - * @param array $remove - * @return void + * {@inheritdoc} * @throws Forbidden */ - public function updateShares(array $add, array $remove) { + public function updateShares(array $add, array $remove): void { if ($this->isShared()) { throw new Forbidden(); } @@ -131,19 +100,16 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable * * readOnly - boolean * * summary - Optional, a description for the share * - * @return array + * @return list<array{href: string, commonName: string, status: int, readOnly: bool, '{http://owncloud.org/ns}principal': string, '{http://owncloud.org/ns}group-share': bool}> */ - public function getShares() { + public function getShares(): array { if ($this->isShared()) { return []; } return $this->caldavBackend->getShares($this->getResourceId()); } - /** - * @return int - */ - public function getResourceId() { + public function getResourceId(): int { return $this->calendarInfo['id']; } @@ -155,7 +121,9 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable } /** - * @return array + * @param int $resourceId + * @param list<array{privilege: string, principal: string, protected: bool}> $acl + * @return list<array{privilege: string, principal: ?string, protected: bool}> */ public function getACL() { $acl = [ @@ -246,16 +214,18 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable parent::getOwner(), 'principals/system/public' ]; - return array_filter($acl, function ($rule) use ($allowedPrincipals) { + /** @var list<array{privilege: string, principal: string, protected: bool}> $acl */ + $acl = array_filter($acl, function (array $rule) use ($allowedPrincipals): bool { return \in_array($rule['principal'], $allowedPrincipals, true); }); + return $acl; } public function getChildACL() { return $this->getACL(); } - public function getOwner() { + public function getOwner(): ?string { if (isset($this->calendarInfo['{http://owncloud.org/ns}owner-principal'])) { return $this->calendarInfo['{http://owncloud.org/ns}owner-principal']; } @@ -400,7 +370,7 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable return isset($this->calendarInfo['{http://owncloud.org/ns}public']); } - protected function isShared() { + public function isShared() { if (!isset($this->calendarInfo['{http://owncloud.org/ns}owner-principal'])) { return false; } @@ -412,6 +382,13 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable return isset($this->calendarInfo['{http://calendarserver.org/ns/}source']); } + public function isDeleted(): bool { + if (!isset($this->calendarInfo[TrashbinPlugin::PROPERTY_DELETED_AT])) { + return false; + } + return $this->calendarInfo[TrashbinPlugin::PROPERTY_DELETED_AT] !== null; + } + /** * @inheritDoc */ @@ -443,7 +420,7 @@ class Calendar extends \Sabre\CalDAV\Calendar implements IRestorable, IShareable } try { - return $this->caldavBackend->moveCalendarObject($sourceNode->getCalendarId(), (int)$this->calendarInfo['id'], $sourceNode->getId(), $sourceNode->getPrincipalUri()); + return $this->caldavBackend->moveCalendarObject($sourceNode->getCalendarId(), (int)$this->calendarInfo['id'], $sourceNode->getId(), $sourceNode->getOwner(), $this->getOwner()); } catch (Exception $e) { $this->logger->error('Could not move calendar object: ' . $e->getMessage(), ['exception' => $e]); return false; diff --git a/apps/dav/lib/CalDAV/CalendarImpl.php b/apps/dav/lib/CalDAV/CalendarImpl.php index 406389e3a3d..27a428a4075 100644 --- a/apps/dav/lib/CalDAV/CalendarImpl.php +++ b/apps/dav/lib/CalDAV/CalendarImpl.php @@ -138,7 +138,7 @@ class CalendarImpl implements ICreateFromString { * Create a new calendar event for this calendar * by way of an ICS string * - * @param string $name the file name - needs to contan the .ics ending + * @param string $name the file name - needs to contain the .ics ending * @param string $calendarData a string containing a valid VEVENT ics * * @throws CalendarException diff --git a/apps/dav/lib/CalDAV/CalendarObject.php b/apps/dav/lib/CalDAV/CalendarObject.php index c927254fba3..32bcff900c2 100644 --- a/apps/dav/lib/CalDAV/CalendarObject.php +++ b/apps/dav/lib/CalDAV/CalendarObject.php @@ -162,4 +162,11 @@ class CalendarObject extends \Sabre\CalDAV\CalendarObject { public function getPrincipalUri(): string { return $this->calendarInfo['principaluri']; } + + public function getOwner(): ?string { + if (isset($this->calendarInfo['{http://owncloud.org/ns}owner-principal'])) { + return $this->calendarInfo['{http://owncloud.org/ns}owner-principal']; + } + return parent::getOwner(); + } } diff --git a/apps/dav/lib/CalDAV/PublicCalendar.php b/apps/dav/lib/CalDAV/PublicCalendar.php index 4a29c8d237a..16c7f86917d 100644 --- a/apps/dav/lib/CalDAV/PublicCalendar.php +++ b/apps/dav/lib/CalDAV/PublicCalendar.php @@ -84,7 +84,7 @@ class PublicCalendar extends Calendar { * public calendars are always shared * @return bool */ - protected function isShared() { + public function isShared() { return true; } } diff --git a/apps/dav/lib/CalDAV/Publishing/Xml/Publisher.php b/apps/dav/lib/CalDAV/Publishing/Xml/Publisher.php index 35bce872bf8..cbff5e66a85 100644 --- a/apps/dav/lib/CalDAV/Publishing/Xml/Publisher.php +++ b/apps/dav/lib/CalDAV/Publishing/Xml/Publisher.php @@ -55,7 +55,7 @@ class Publisher implements XmlSerializable { } /** - * The xmlSerialize metod is called during xml writing. + * The xmlSerialize method is called during xml writing. * * Use the $writer argument to write its own xml serialization. * diff --git a/apps/dav/lib/CalDAV/Schedule/Plugin.php b/apps/dav/lib/CalDAV/Schedule/Plugin.php index 96bacce4454..44517541faa 100644 --- a/apps/dav/lib/CalDAV/Schedule/Plugin.php +++ b/apps/dav/lib/CalDAV/Schedule/Plugin.php @@ -30,6 +30,7 @@ namespace OCA\DAV\CalDAV\Schedule; use DateTimeZone; use OCA\DAV\CalDAV\CalDavBackend; +use OCA\DAV\CalDAV\Calendar; use OCA\DAV\CalDAV\CalendarHome; use OCP\IConfig; use Sabre\CalDAV\ICalendar; @@ -178,7 +179,7 @@ class Plugin extends \Sabre\CalDAV\Schedule\Plugin { // If parent::scheduleLocalDelivery set scheduleStatus to 1.2, // it means that it was successfully delivered locally. - // Meaning that the ACL plugin is loaded and that a principial + // Meaning that the ACL plugin is loaded and that a principal // exists for the given recipient id, no need to double check /** @var \Sabre\DAVACL\Plugin $aclPlugin */ $aclPlugin = $this->server->getPlugin('acl'); @@ -299,12 +300,14 @@ EOF; return null; } + $isResourceOrRoom = strpos($principalUrl, 'principals/calendar-resources') === 0 || + strpos($principalUrl, 'principals/calendar-rooms') === 0; + if (strpos($principalUrl, 'principals/users') === 0) { [, $userId] = split($principalUrl); $uri = $this->config->getUserValue($userId, 'dav', 'defaultCalendar', CalDavBackend::PERSONAL_CALENDAR_URI); $displayName = CalDavBackend::PERSONAL_CALENDAR_NAME; - } elseif (strpos($principalUrl, 'principals/calendar-resources') === 0 || - strpos($principalUrl, 'principals/calendar-rooms') === 0) { + } elseif ($isResourceOrRoom) { $uri = CalDavBackend::RESOURCE_BOOKING_CALENDAR_URI; $displayName = CalDavBackend::RESOURCE_BOOKING_CALENDAR_NAME; } else { @@ -316,9 +319,40 @@ EOF; /** @var CalendarHome $calendarHome */ $calendarHome = $this->server->tree->getNodeForPath($calendarHomePath); if (!$calendarHome->childExists($uri)) { - $calendarHome->getCalDAVBackend()->createCalendar($principalUrl, $uri, [ - '{DAV:}displayname' => $displayName, - ]); + // If the default calendar doesn't exist + if ($isResourceOrRoom) { + $calendarHome->getCalDAVBackend()->createCalendar($principalUrl, $uri, [ + '{DAV:}displayname' => $displayName, + ]); + } else { + // And we're not handling scheduling on resource/room booking + $userCalendars = []; + /** + * If the default calendar of the user isn't set and the + * fallback doesn't match any of the user's calendar + * try to find the first "personal" calendar we can write to + * instead of creating a new one. + * A appropriate personal calendar to receive invites: + * - isn't a calendar subscription + * - user can write to it (no virtual/3rd-party calendars) + * - calendar isn't a share + */ + foreach ($calendarHome->getChildren() as $node) { + if ($node instanceof Calendar && !$node->isSubscription() && $node->canWrite() && !$node->isShared() && !$node->isDeleted()) { + $userCalendars[] = $node; + } + } + + if (count($userCalendars) > 0) { + // Calendar backend returns calendar by calendarorder property + $uri = $userCalendars[0]->getName(); + } else { + // Otherwise if we have really nothing, create a new calendar + $calendarHome->getCalDAVBackend()->createCalendar($principalUrl, $uri, [ + '{DAV:}displayname' => $displayName, + ]); + } + } } $result = $this->server->getPropertiesForPath($calendarHomePath . '/' . $uri, [], 1); @@ -533,7 +567,7 @@ EOF; } // If more than one Free-Busy property was returned, it means that an event - // starts or ends inside this time-range, so it's not availabe and we return false + // starts or ends inside this time-range, so it's not available and we return false if (count($freeBusyProperties) > 1) { return false; } diff --git a/apps/dav/lib/CalDAV/WebcalCaching/RefreshWebcalService.php b/apps/dav/lib/CalDAV/WebcalCaching/RefreshWebcalService.php index 0dbe7398f52..eadeea3457c 100644 --- a/apps/dav/lib/CalDAV/WebcalCaching/RefreshWebcalService.php +++ b/apps/dav/lib/CalDAV/WebcalCaching/RefreshWebcalService.php @@ -56,14 +56,15 @@ use function count; class RefreshWebcalService { /** @var CalDavBackend */ - private $calDavBackend; + private CalDavBackend $calDavBackend; /** @var IClientService */ - private $clientService; + private IClientService $clientService; /** @var IConfig */ - private $config; + private IConfig $config; + /** @var LoggerInterface */ private LoggerInterface $logger; public const REFRESH_RATE = '{http://apple.com/ns/ical/}refreshrate'; @@ -71,13 +72,7 @@ class RefreshWebcalService { public const STRIP_ATTACHMENTS = '{http://calendarserver.org/ns/}subscribed-strip-attachments'; public const STRIP_TODOS = '{http://calendarserver.org/ns/}subscribed-strip-todos'; - /** - * RefreshWebcalJob constructor. - */ - public function __construct(CalDavBackend $calDavBackend, - IClientService $clientService, - IConfig $config, - LoggerInterface $logger) { + public function __construct(CalDavBackend $calDavBackend, IClientService $clientService, IConfig $config, LoggerInterface $logger) { $this->calDavBackend = $calDavBackend; $this->clientService = $clientService; $this->config = $config; @@ -131,12 +126,12 @@ class RefreshWebcalService { continue; } - $uri = $this->getRandomCalendarObjectUri(); + $objectUri = $this->getRandomCalendarObjectUri(); $calendarData = $vObject->serialize(); try { - $this->calDavBackend->createCalendarObject($subscription['id'], $uri, $calendarData, CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION); + $this->calDavBackend->createCalendarObject($subscription['id'], $objectUri, $calendarData, CalDavBackend::CALENDAR_TYPE_SUBSCRIPTION); } catch (NoInstancesException | BadRequest $ex) { - $this->logger->error($ex->getMessage(), ['exception' => $ex]); + $this->logger->error('Unable to create calendar object from subscription {subscriptionId}', ['exception' => $ex, 'subscriptionId' => $subscription['id'], 'source' => $subscription['source']]); } } @@ -147,20 +142,14 @@ class RefreshWebcalService { $this->updateSubscription($subscription, $mutations); } catch (ParseException $ex) { - $subscriptionId = $subscription['id']; - - $this->logger->error("Subscription $subscriptionId could not be refreshed due to a parsing error", ['exception' => $ex]); + $this->logger->error("Subscription {subscriptionId} could not be refreshed due to a parsing error", ['exception' => $ex, 'subscriptionId' => $subscription['id']]); } } /** * loads subscription from backend - * - * @param string $principalUri - * @param string $uri - * @return array|null */ - public function getSubscription(string $principalUri, string $uri) { + public function getSubscription(string $principalUri, string $uri): ?array { $subscriptions = array_values(array_filter( $this->calDavBackend->getSubscriptionsForUser($principalUri), function ($sub) use ($uri) { @@ -177,12 +166,8 @@ class RefreshWebcalService { /** * gets webcal feed from remote server - * - * @param array $subscription - * @param array &$mutations - * @return null|string */ - private function queryWebcalFeed(array $subscription, array &$mutations) { + private function queryWebcalFeed(array $subscription, array &$mutations): ?string { $client = $this->clientService->newClient(); $didBreak301Chain = false; @@ -244,7 +229,7 @@ class RefreshWebcalService { $jCalendar = Reader::readJson($body, Reader::OPTION_FORGIVING); } catch (Exception $ex) { // In case of a parsing error return null - $this->logger->debug("Subscription $subscriptionId could not be parsed"); + $this->logger->warning("Subscription $subscriptionId could not be parsed", ['exception' => $ex]); return null; } return $jCalendar->serialize(); @@ -254,7 +239,7 @@ class RefreshWebcalService { $xCalendar = Reader::readXML($body); } catch (Exception $ex) { // In case of a parsing error return null - $this->logger->debug("Subscription $subscriptionId could not be parsed"); + $this->logger->warning("Subscription $subscriptionId could not be parsed", ['exception' => $ex]); return null; } return $xCalendar->serialize(); @@ -265,7 +250,7 @@ class RefreshWebcalService { $vCalendar = Reader::read($body); } catch (Exception $ex) { // In case of a parsing error return null - $this->logger->debug("Subscription $subscriptionId could not be parsed"); + $this->logger->warning("Subscription $subscriptionId could not be parsed", ['exception' => $ex]); return null; } return $vCalendar->serialize(); @@ -291,11 +276,8 @@ class RefreshWebcalService { * - the webcal feed suggests a refreshrate * - return suggested refreshrate if user didn't set a custom one * - * @param array $subscription - * @param string $webcalData - * @return string|null */ - private function checkWebcalDataForRefreshRate($subscription, $webcalData) { + private function checkWebcalDataForRefreshRate(array $subscription, string $webcalData): ?string { // if there is no refreshrate stored in the database, check the webcal feed // whether it suggests any refresh rate and store that in the database if (isset($subscription[self::REFRESH_RATE]) && $subscription[self::REFRESH_RATE] !== null) { @@ -353,7 +335,7 @@ class RefreshWebcalService { * @param string $url * @return string|null */ - private function cleanURL(string $url) { + private function cleanURL(string $url): ?string { $parsed = parse_url($url); if ($parsed === false) { return null; diff --git a/apps/dav/lib/CardDAV/Activity/Backend.php b/apps/dav/lib/CardDAV/Activity/Backend.php index b713284e182..8c8c643e287 100644 --- a/apps/dav/lib/CardDAV/Activity/Backend.php +++ b/apps/dav/lib/CardDAV/Activity/Backend.php @@ -32,6 +32,7 @@ use OCP\App\IAppManager; use OCP\IGroup; use OCP\IGroupManager; use OCP\IUser; +use OCP\IUserManager; use OCP\IUserSession; use Sabre\CardDAV\Plugin; use Sabre\VObject\Reader; @@ -50,14 +51,19 @@ class Backend { /** @var IAppManager */ protected $appManager; + /** @var IUserManager */ + protected $userManager; + public function __construct(IActivityManager $activityManager, IGroupManager $groupManager, IUserSession $userSession, - IAppManager $appManager) { + IAppManager $appManager, + IUserManager $userManager) { $this->activityManager = $activityManager; $this->groupManager = $groupManager; $this->userSession = $userSession; $this->appManager = $appManager; + $this->userManager = $userManager; } /** @@ -103,7 +109,14 @@ class Backend { return; } - $principal = explode('/', $addressbookData['principaluri']); + $principalUri = $addressbookData['principaluri']; + + // We are not interested in changes from the system addressbook + if ($principalUri === 'principals/system/system') { + return; + } + + $principal = explode('/', $principalUri); $owner = array_pop($principal); $currentUser = $this->userSession->getUser(); @@ -132,6 +145,11 @@ class Backend { } foreach ($users as $user) { + if ($action === Addressbook::SUBJECT_DELETE && !$this->userManager->userExists($user)) { + // Avoid creating addressbook_delete activities for deleted users + continue; + } + $event->setAffectedUser($user) ->setSubject( $user === $currentUser ? $action . '_self' : $action, @@ -393,7 +411,14 @@ class Backend { return; } - $principal = explode('/', $addressbookData['principaluri']); + $principalUri = $addressbookData['principaluri']; + + // We are not interested in changes from the system addressbook + if ($principalUri === 'principals/system/system') { + return; + } + + $principal = explode('/', $principalUri); $owner = array_pop($principal); $currentUser = $this->userSession->getUser(); diff --git a/apps/dav/lib/CardDAV/AddressBook.php b/apps/dav/lib/CardDAV/AddressBook.php index 9bd24bedbac..bca478feec1 100644 --- a/apps/dav/lib/CardDAV/AddressBook.php +++ b/apps/dav/lib/CardDAV/AddressBook.php @@ -67,17 +67,15 @@ class AddressBook extends \Sabre\CardDAV\AddressBook implements IShareable { * Every element in the add array has the following properties: * * href - A url. Usually a mailto: address * * commonName - Usually a first and last name, or false - * * summary - A description of the share, can also be false * * readOnly - A boolean value * * Every element in the remove array is just the address string. * - * @param array $add - * @param array $remove - * @return void + * @param list<array{href: string, commonName: string, readOnly: bool}> $add + * @param list<string> $remove * @throws Forbidden */ - public function updateShares(array $add, array $remove) { + public function updateShares(array $add, array $remove): void { if ($this->isShared()) { throw new Forbidden(); } @@ -92,11 +90,10 @@ class AddressBook extends \Sabre\CardDAV\AddressBook implements IShareable { * * commonName - Optional, for example a first + last name * * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants. * * readOnly - boolean - * * summary - Optional, a description for the share * - * @return array + * @return list<array{href: string, commonName: string, status: int, readOnly: bool, '{http://owncloud.org/ns}principal': string, '{http://owncloud.org/ns}group-share': bool}> */ - public function getShares() { + public function getShares(): array { if ($this->isShared()) { return []; } @@ -163,14 +160,11 @@ class AddressBook extends \Sabre\CardDAV\AddressBook implements IShareable { return new Card($this->carddavBackend, $this->addressBookInfo, $obj); } - /** - * @return int - */ - public function getResourceId() { + public function getResourceId(): int { return $this->addressBookInfo['id']; } - public function getOwner() { + public function getOwner(): ?string { if (isset($this->addressBookInfo['{http://owncloud.org/ns}owner-principal'])) { return $this->addressBookInfo['{http://owncloud.org/ns}owner-principal']; } @@ -207,7 +201,7 @@ class AddressBook extends \Sabre\CardDAV\AddressBook implements IShareable { return $this->carddavBackend->collectCardProperties($this->getResourceId(), 'CATEGORIES'); } - private function isShared() { + private function isShared(): bool { if (!isset($this->addressBookInfo['{http://owncloud.org/ns}owner-principal'])) { return false; } @@ -215,7 +209,7 @@ class AddressBook extends \Sabre\CardDAV\AddressBook implements IShareable { return $this->addressBookInfo['{http://owncloud.org/ns}owner-principal'] !== $this->addressBookInfo['principaluri']; } - private function canWrite() { + private function canWrite(): bool { if (isset($this->addressBookInfo['{http://owncloud.org/ns}read-only'])) { return !$this->addressBookInfo['{http://owncloud.org/ns}read-only']; } diff --git a/apps/dav/lib/CardDAV/AddressBookImpl.php b/apps/dav/lib/CardDAV/AddressBookImpl.php index 3db20cb4220..2f7f0a22900 100644 --- a/apps/dav/lib/CardDAV/AddressBookImpl.php +++ b/apps/dav/lib/CardDAV/AddressBookImpl.php @@ -12,6 +12,7 @@ * @author Joas Schilling <coding@schilljs.com> * @author John Molakvoæ <skjnldsv@protonmail.com> * @author Julius Härtl <jus@bitgrid.net> + * @author Thomas Citharel <nextcloud@tcit.fr> * @author Thomas Müller <thomas.mueller@tmit.eu> * * @license AGPL-3.0 @@ -206,7 +207,7 @@ class AddressBookImpl implements IAddressBook { } /** - * @param object $id the unique identifier to a contact + * @param int $id the unique identifier to a contact * @return bool successful or not * @since 5.0.0 */ diff --git a/apps/dav/lib/CardDAV/CardDavBackend.php b/apps/dav/lib/CardDAV/CardDavBackend.php index 864f3da9367..a9ca2eb30a3 100644 --- a/apps/dav/lib/CardDAV/CardDavBackend.php +++ b/apps/dav/lib/CardDAV/CardDavBackend.php @@ -58,30 +58,19 @@ use Sabre\CardDAV\Plugin; use Sabre\DAV\Exception\BadRequest; use Sabre\VObject\Component\VCard; use Sabre\VObject\Reader; -use Symfony\Component\EventDispatcher\EventDispatcherInterface; -use Symfony\Component\EventDispatcher\GenericEvent; class CardDavBackend implements BackendInterface, SyncSupport { public const PERSONAL_ADDRESSBOOK_URI = 'contacts'; public const PERSONAL_ADDRESSBOOK_NAME = 'Contacts'; - /** @var Principal */ - private $principalBackend; - - /** @var string */ - private $dbCardsTable = 'cards'; - - /** @var string */ - private $dbCardsPropertiesTable = 'cards_properties'; - - /** @var IDBConnection */ - private $db; - - /** @var Backend */ - private $sharingBackend; + private Principal $principalBackend; + private string $dbCardsTable = 'cards'; + private string $dbCardsPropertiesTable = 'cards_properties'; + private IDBConnection $db; + private Backend $sharingBackend; /** @var array properties to index */ - public static $indexProperties = [ + public static array $indexProperties = [ 'BDAY', 'UID', 'N', 'FN', 'TITLE', 'ROLE', 'NOTE', 'NICKNAME', 'ORG', 'CATEGORIES', 'EMAIL', 'TEL', 'IMPP', 'ADR', 'URL', 'GEO', 'CLOUD', 'X-SOCIALPROFILE']; @@ -89,18 +78,10 @@ class CardDavBackend implements BackendInterface, SyncSupport { /** * @var string[] Map of uid => display name */ - protected $userDisplayNames; - - /** @var IUserManager */ - private $userManager; - - /** @var IEventDispatcher */ - private $dispatcher; - - /** @var EventDispatcherInterface */ - private $legacyDispatcher; - - private $etagCache = []; + protected array $userDisplayNames; + private IUserManager $userManager; + private IEventDispatcher $dispatcher; + private array $etagCache = []; /** * CardDavBackend constructor. @@ -110,19 +91,16 @@ class CardDavBackend implements BackendInterface, SyncSupport { * @param IUserManager $userManager * @param IGroupManager $groupManager * @param IEventDispatcher $dispatcher - * @param EventDispatcherInterface $legacyDispatcher */ public function __construct(IDBConnection $db, Principal $principalBackend, IUserManager $userManager, IGroupManager $groupManager, - IEventDispatcher $dispatcher, - EventDispatcherInterface $legacyDispatcher) { + IEventDispatcher $dispatcher) { $this->db = $db; $this->principalBackend = $principalBackend; $this->userManager = $userManager; $this->dispatcher = $dispatcher; - $this->legacyDispatcher = $legacyDispatcher; $this->sharingBackend = new Backend($this->db, $this->userManager, $groupManager, $principalBackend, 'addressbook'); } @@ -318,18 +296,14 @@ class CardDavBackend implements BackendInterface, SyncSupport { return $addressBook; } - /** - * @param $addressBookUri - * @return array|null - */ - public function getAddressBooksByUri($principal, $addressBookUri) { + public function getAddressBooksByUri(string $principal, string $addressBookUri): ?array { $query = $this->db->getQueryBuilder(); $result = $query->select(['id', 'uri', 'displayname', 'principaluri', 'description', 'synctoken']) ->from('addressbooks') ->where($query->expr()->eq('uri', $query->createNamedParameter($addressBookUri))) ->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($principal))) ->setMaxResults(1) - ->execute(); + ->executeQuery(); $row = $result->fetch(); $result->closeCursor(); @@ -393,12 +367,12 @@ class CardDavBackend implements BackendInterface, SyncSupport { $query->set($key, $query->createNamedParameter($value)); } $query->where($query->expr()->eq('id', $query->createNamedParameter($addressBookId))) - ->execute(); + ->executeStatement(); $this->addChange($addressBookId, "", 2); $addressBookRow = $this->getAddressBookById((int)$addressBookId); - $shares = $this->getShares($addressBookId); + $shares = $this->getShares((int)$addressBookId); $this->dispatcher->dispatchTyped(new AddressBookUpdatedEvent((int)$addressBookId, $addressBookRow, $shares, $mutations)); return true; @@ -468,30 +442,31 @@ class CardDavBackend implements BackendInterface, SyncSupport { * @return void */ public function deleteAddressBook($addressBookId) { + $addressBookId = (int)$addressBookId; $addressBookData = $this->getAddressBookById($addressBookId); $shares = $this->getShares($addressBookId); $query = $this->db->getQueryBuilder(); $query->delete($this->dbCardsTable) ->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid'))) - ->setParameter('addressbookid', $addressBookId) - ->execute(); + ->setParameter('addressbookid', $addressBookId, IQueryBuilder::PARAM_INT) + ->executeStatement(); $query->delete('addressbookchanges') ->where($query->expr()->eq('addressbookid', $query->createParameter('addressbookid'))) - ->setParameter('addressbookid', $addressBookId) - ->execute(); + ->setParameter('addressbookid', $addressBookId, IQueryBuilder::PARAM_INT) + ->executeStatement(); $query->delete('addressbooks') ->where($query->expr()->eq('id', $query->createParameter('id'))) - ->setParameter('id', $addressBookId) - ->execute(); + ->setParameter('id', $addressBookId, IQueryBuilder::PARAM_INT) + ->executeStatement(); $this->sharingBackend->deleteAllShares($addressBookId); $query->delete($this->dbCardsPropertiesTable) - ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId))) - ->execute(); + ->where($query->expr()->eq('addressbookid', $query->createNamedParameter($addressBookId, IQueryBuilder::PARAM_INT))) + ->executeStatement(); if ($addressBookData) { $this->dispatcher->dispatchTyped(new AddressBookDeletedEvent($addressBookId, $addressBookData, $shares)); @@ -511,7 +486,7 @@ class CardDavBackend implements BackendInterface, SyncSupport { * * size - The size of the card in bytes. * * If these last two properties are provided, less time will be spent - * calculating them. If they are specified, you can also ommit carddata. + * calculating them. If they are specified, you can also omit carddata. * This may speed up certain requests, especially with large cards. * * @param mixed $addressbookId @@ -692,11 +667,6 @@ class CardDavBackend implements BackendInterface, SyncSupport { $shares = $this->getShares($addressBookId); $objectRow = $this->getCard($addressBookId, $cardUri); $this->dispatcher->dispatchTyped(new CardCreatedEvent($addressBookId, $addressBookData, $shares, $objectRow)); - $this->legacyDispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::createCard', - new GenericEvent(null, [ - 'addressBookId' => $addressBookId, - 'cardUri' => $cardUri, - 'cardData' => $cardData])); return '"' . $etag . '"'; } @@ -756,12 +726,6 @@ class CardDavBackend implements BackendInterface, SyncSupport { $shares = $this->getShares($addressBookId); $objectRow = $this->getCard($addressBookId, $cardUri); $this->dispatcher->dispatchTyped(new CardUpdatedEvent($addressBookId, $addressBookData, $shares, $objectRow)); - $this->legacyDispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::updateCard', - new GenericEvent(null, [ - 'addressBookId' => $addressBookId, - 'cardUri' => $cardUri, - 'cardData' => $cardData])); - return '"' . $etag . '"'; } @@ -793,11 +757,6 @@ class CardDavBackend implements BackendInterface, SyncSupport { if ($ret === 1) { if ($cardId !== null) { $this->dispatcher->dispatchTyped(new CardDeletedEvent($addressBookId, $addressBookData, $shares, $objectRow)); - $this->legacyDispatcher->dispatch('\OCA\DAV\CardDAV\CardDavBackend::deleteCard', - new GenericEvent(null, [ - 'addressBookId' => $addressBookId, - 'cardUri' => $cardUri])); - $this->purgeProperties($addressBookId, $cardId); } return true; @@ -1002,11 +961,10 @@ class CardDavBackend implements BackendInterface, SyncSupport { } /** - * @param IShareable $shareable - * @param string[] $add - * @param string[] $remove + * @param list<array{href: string, commonName: string, readOnly: bool}> $add + * @param list<string> $remove */ - public function updateShares(IShareable $shareable, $add, $remove) { + public function updateShares(IShareable $shareable, array $add, array $remove): void { $addressBookId = $shareable->getResourceId(); $addressBookData = $this->getAddressBookById($addressBookId); $oldShares = $this->getShares($addressBookId); @@ -1237,11 +1195,10 @@ class CardDavBackend implements BackendInterface, SyncSupport { * * commonName - Optional, for example a first + last name * * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants. * * readOnly - boolean - * * summary - Optional, a description for the share * - * @return array + * @return list<array{href: string, commonName: string, status: int, readOnly: bool, '{http://owncloud.org/ns}principal': string, '{http://owncloud.org/ns}group-share': bool}> */ - public function getShares($addressBookId) { + public function getShares(int $addressBookId): array { return $this->sharingBackend->getShares($addressBookId); } @@ -1321,13 +1278,9 @@ class CardDavBackend implements BackendInterface, SyncSupport { } /** - * get ID from a given contact - * - * @param int $addressBookId - * @param string $uri - * @return int + * Get ID from a given contact */ - protected function getCardId($addressBookId, $uri) { + protected function getCardId(int $addressBookId, string $uri): int { $query = $this->db->getQueryBuilder(); $query->select('id')->from($this->dbCardsTable) ->where($query->expr()->eq('uri', $query->createNamedParameter($uri))) @@ -1347,15 +1300,15 @@ class CardDavBackend implements BackendInterface, SyncSupport { /** * For shared address books the sharee is set in the ACL of the address book * - * @param $addressBookId - * @param $acl - * @return array + * @param int $addressBookId + * @param list<array{privilege: string, principal: string, protected: bool}> $acl + * @return list<array{privilege: string, principal: string, protected: bool}> */ - public function applyShareAcl($addressBookId, $acl) { + public function applyShareAcl(int $addressBookId, array $acl): array { return $this->sharingBackend->applyShareAcl($addressBookId, $acl); } - private function convertPrincipal($principalUri, $toV2) { + private function convertPrincipal(string $principalUri, bool $toV2): string { if ($this->principalBackend->getPrincipalPrefix() === 'principals') { [, $name] = \Sabre\Uri\split($principalUri); if ($toV2 === true) { @@ -1366,7 +1319,7 @@ class CardDavBackend implements BackendInterface, SyncSupport { return $principalUri; } - private function addOwnerPrincipal(&$addressbookInfo) { + private function addOwnerPrincipal(array &$addressbookInfo): void { $ownerPrincipalKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_OWNCLOUD . '}owner-principal'; $displaynameKey = '{' . \OCA\DAV\DAV\Sharing\Plugin::NS_NEXTCLOUD . '}owner-displayname'; if (isset($addressbookInfo[$ownerPrincipalKey])) { @@ -1386,10 +1339,10 @@ class CardDavBackend implements BackendInterface, SyncSupport { * * @param string $cardData the vcard raw data * @return string the uid - * @throws BadRequest if no UID is available + * @throws BadRequest if no UID is available or vcard is empty */ - private function getUID($cardData) { - if ($cardData != '') { + private function getUID(string $cardData): string { + if ($cardData !== '') { $vCard = Reader::read($cardData); if ($vCard->UID) { $uid = $vCard->UID->getValue(); diff --git a/apps/dav/lib/CardDAV/SyncService.php b/apps/dav/lib/CardDAV/SyncService.php index 169dbc79e0f..5094b7f3f5c 100644 --- a/apps/dav/lib/CardDAV/SyncService.php +++ b/apps/dav/lib/CardDAV/SyncService.php @@ -40,27 +40,13 @@ use Sabre\HTTP\ClientHttpException; use Sabre\VObject\Reader; class SyncService { - - /** @var CardDavBackend */ - private $backend; - - /** @var IUserManager */ - private $userManager; - + private CardDavBackend $backend; + private IUserManager $userManager; private LoggerInterface $logger; + private ?array $localSystemAddressBook = null; + private Converter $converter; + protected string $certPath; - /** @var array */ - private $localSystemAddressBook; - - /** @var Converter */ - private $converter; - - /** @var string */ - protected $certPath; - - /** - * SyncService constructor. - */ public function __construct(CardDavBackend $backend, IUserManager $userManager, LoggerInterface $logger, @@ -73,20 +59,11 @@ class SyncService { } /** - * @param string $url - * @param string $userName - * @param string $addressBookUrl - * @param string $sharedSecret - * @param string $syncToken - * @param int $targetBookId - * @param string $targetPrincipal - * @param array $targetProperties - * @return string * @throws \Exception */ - public function syncRemoteAddressBook($url, $userName, $addressBookUrl, $sharedSecret, $syncToken, $targetBookId, $targetPrincipal, $targetProperties) { + public function syncRemoteAddressBook(string $url, string $userName, string $addressBookUrl, string $sharedSecret, ?string $syncToken, string $targetBookHash, string $targetPrincipal, array $targetProperties): string { // 1. create addressbook - $book = $this->ensureSystemAddressBookExists($targetPrincipal, (string)$targetBookId, $targetProperties); + $book = $this->ensureSystemAddressBookExists($targetPrincipal, $targetBookHash, $targetProperties); $addressBookId = $book['id']; // 2. query changes @@ -122,28 +99,23 @@ class SyncService { } /** - * @param string $principal - * @param string $id - * @param array $properties - * @return array|null * @throws \Sabre\DAV\Exception\BadRequest */ - public function ensureSystemAddressBookExists($principal, $id, $properties) { - $book = $this->backend->getAddressBooksByUri($principal, $id); + public function ensureSystemAddressBookExists(string $principal, string $uri, array $properties): ?array { + $book = $this->backend->getAddressBooksByUri($principal, $uri); if (!is_null($book)) { return $book; } - $this->backend->createAddressBook($principal, $id, $properties); + // FIXME This might break in clustered DB setup + $this->backend->createAddressBook($principal, $uri, $properties); - return $this->backend->getAddressBooksByUri($principal, $id); + return $this->backend->getAddressBooksByUri($principal, $uri); } /** * Check if there is a valid certPath we should use - * - * @return string */ - protected function getCertPath() { + protected function getCertPath(): string { // we already have a valid certPath if ($this->certPath !== '') { @@ -159,14 +131,7 @@ class SyncService { return $this->certPath; } - /** - * @param string $url - * @param string $userName - * @param string $addressBookUrl - * @param string $sharedSecret - * @return Client - */ - protected function getClient($url, $userName, $sharedSecret) { + protected function getClient(string $url, string $userName, string $sharedSecret): Client { $settings = [ 'baseUri' => $url . '/', 'userName' => $userName, @@ -183,15 +148,7 @@ class SyncService { return $client; } - /** - * @param string $url - * @param string $userName - * @param string $addressBookUrl - * @param string $sharedSecret - * @param string $syncToken - * @return array - */ - protected function requestSyncReport($url, $userName, $addressBookUrl, $sharedSecret, $syncToken) { + protected function requestSyncReport(string $url, string $userName, string $addressBookUrl, string $sharedSecret, ?string $syncToken): array { $client = $this->getClient($url, $userName, $sharedSecret); $body = $this->buildSyncCollectionRequestBody($syncToken); @@ -203,23 +160,12 @@ class SyncService { return $this->parseMultiStatus($response['body']); } - /** - * @param string $url - * @param string $userName - * @param string $sharedSecret - * @param string $resourcePath - * @return array - */ - protected function download($url, $userName, $sharedSecret, $resourcePath) { + protected function download(string $url, string $userName, string $sharedSecret, string $resourcePath): array { $client = $this->getClient($url, $userName, $sharedSecret); return $client->request('GET', $resourcePath); } - /** - * @param string|null $syncToken - * @return string - */ - private function buildSyncCollectionRequestBody($syncToken) { + private function buildSyncCollectionRequestBody(?string $syncToken): string { $dom = new \DOMDocument('1.0', 'UTF-8'); $dom->formatOutput = true; $root = $dom->createElementNS('DAV:', 'd:sync-collection'); diff --git a/apps/dav/lib/Command/CreateCalendar.php b/apps/dav/lib/Command/CreateCalendar.php index 2bea82a345e..08f937dff9d 100644 --- a/apps/dav/lib/Command/CreateCalendar.php +++ b/apps/dav/lib/Command/CreateCalendar.php @@ -29,6 +29,7 @@ use OC\KnownUser\KnownUserService; use OCA\DAV\CalDAV\CalDavBackend; use OCA\DAV\CalDAV\Proxy\ProxyMapper; use OCA\DAV\Connector\Sabre\Principal; +use OCP\Accounts\IAccountManager; use OCP\EventDispatcher\IEventDispatcher; use OCP\IConfig; use OCP\IDBConnection; @@ -83,6 +84,7 @@ class CreateCalendar extends Command { $principalBackend = new Principal( $this->userManager, $this->groupManager, + \OC::$server->get(IAccountManager::class), \OC::$server->getShareManager(), \OC::$server->getUserSession(), \OC::$server->getAppManager(), @@ -94,7 +96,6 @@ class CreateCalendar extends Command { $random = \OC::$server->getSecureRandom(); $logger = \OC::$server->get(LoggerInterface::class); $dispatcher = \OC::$server->get(IEventDispatcher::class); - $legacyDispatcher = \OC::$server->getEventDispatcher(); $config = \OC::$server->get(IConfig::class); $name = $input->getArgument('name'); @@ -106,7 +107,6 @@ class CreateCalendar extends Command { $random, $logger, $dispatcher, - $legacyDispatcher, $config ); $caldav->createCalendar("principals/users/$user", $name, []); diff --git a/apps/dav/lib/Command/MoveCalendar.php b/apps/dav/lib/Command/MoveCalendar.php index 320fe8aeac6..9272b20b10d 100644 --- a/apps/dav/lib/Command/MoveCalendar.php +++ b/apps/dav/lib/Command/MoveCalendar.php @@ -42,41 +42,17 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; class MoveCalendar extends Command { - - /** @var IUserManager */ - private $userManager; - - /** @var IGroupManager */ - private $groupManager; - - /** @var IShareManager */ - private $shareManager; - - /** @var IConfig $config */ - private $config; - - /** @var IL10N */ - private $l10n; - - /** @var SymfonyStyle */ - private $io; - - /** @var CalDavBackend */ - private $calDav; - - /** @var LoggerInterface */ - private $logger; + private IUserManager $userManager; + private IGroupManager $groupManager; + private IShareManager $shareManager; + private IConfig $config; + private IL10N $l10n; + private ?SymfonyStyle $io = null; + private CalDavBackend $calDav; + private LoggerInterface $logger; public const URI_USERS = 'principals/users/'; - /** - * @param IUserManager $userManager - * @param IGroupManager $groupManager - * @param IShareManager $shareManager - * @param IConfig $config - * @param IL10N $l10n - * @param CalDavBackend $calDav - */ public function __construct( IUserManager $userManager, IGroupManager $groupManager, @@ -224,7 +200,7 @@ class MoveCalendar extends Command { */ if ($this->shareManager->shareWithGroupMembersOnly() === true && 'groups' === $prefix && !$this->groupManager->isInGroup($userDestination, $userOrGroup)) { if ($force) { - $this->calDav->updateShares(new Calendar($this->calDav, $calendar, $this->l10n, $this->config, $this->logger), [], ['href' => 'principal:principals/groups/' . $userOrGroup]); + $this->calDav->updateShares(new Calendar($this->calDav, $calendar, $this->l10n, $this->config, $this->logger), [], ['principal:principals/groups/' . $userOrGroup]); } else { throw new \InvalidArgumentException("User <$userDestination> is not part of the group <$userOrGroup> with whom the calendar <" . $calendar['uri'] . "> was shared. You may use -f to move the calendar while deleting this share."); } @@ -235,7 +211,7 @@ class MoveCalendar extends Command { */ if ($userOrGroup === $userDestination) { if ($force) { - $this->calDav->updateShares(new Calendar($this->calDav, $calendar, $this->l10n, $this->config, $this->logger), [], ['href' => 'principal:principals/users/' . $userOrGroup]); + $this->calDav->updateShares(new Calendar($this->calDav, $calendar, $this->l10n, $this->config, $this->logger), [], ['principal:principals/users/' . $userOrGroup]); } else { throw new \InvalidArgumentException("The calendar <" . $calendar['uri'] . "> is already shared to user <$userDestination>.You may use -f to move the calendar while deleting this share."); } diff --git a/apps/dav/lib/Connector/Sabre/ChecksumList.php b/apps/dav/lib/Connector/Sabre/ChecksumList.php index 344e6a4ab3c..44573de03c8 100644 --- a/apps/dav/lib/Connector/Sabre/ChecksumList.php +++ b/apps/dav/lib/Connector/Sabre/ChecksumList.php @@ -42,7 +42,7 @@ class ChecksumList implements XmlSerializable { } /** - * The xmlSerialize metod is called during xml writing. + * The xmlSerialize method is called during xml writing. * * Use the $writer argument to write its own xml serialization. * diff --git a/apps/dav/lib/Connector/Sabre/Directory.php b/apps/dav/lib/Connector/Sabre/Directory.php index 5280511d5be..b575a051b2a 100644 --- a/apps/dav/lib/Connector/Sabre/Directory.php +++ b/apps/dav/lib/Connector/Sabre/Directory.php @@ -66,7 +66,7 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol /** Cached quota info */ private ?array $quotaInfo = null; - private ?ObjectTree $tree = null; + private ?CachingTree $tree = null; /** @var array<string, array<int, FileMetadata>> */ private array $metadata = []; @@ -74,7 +74,7 @@ class Directory extends \OCA\DAV\Connector\Sabre\Node implements \Sabre\DAV\ICol /** * Sets up the node, expects a full path name */ - public function __construct(View $view, FileInfo $info, ?ObjectTree $tree = null, IShareManager $shareManager = null) { + public function __construct(View $view, FileInfo $info, ?CachingTree $tree = null, IShareManager $shareManager = null) { parent::__construct($view, $info, $shareManager); $this->tree = $tree; } diff --git a/apps/dav/lib/Connector/Sabre/ExceptionLoggerPlugin.php b/apps/dav/lib/Connector/Sabre/ExceptionLoggerPlugin.php index ea94b5c8933..ebf3e4021eb 100644 --- a/apps/dav/lib/Connector/Sabre/ExceptionLoggerPlugin.php +++ b/apps/dav/lib/Connector/Sabre/ExceptionLoggerPlugin.php @@ -29,6 +29,7 @@ namespace OCA\DAV\Connector\Sabre; use OCA\DAV\Connector\Sabre\Exception\FileLocked; use OCA\DAV\Connector\Sabre\Exception\PasswordLoginForbidden; +use OCA\DAV\Exception\ServerMaintenanceMode; use OCP\Files\StorageNotAvailableException; use Psr\Log\LoggerInterface; use Sabre\DAV\Exception\BadRequest; @@ -81,6 +82,7 @@ class ExceptionLoggerPlugin extends \Sabre\DAV\ServerPlugin { FileLocked::class => true, // An invalid range is requested RequestedRangeNotSatisfiable::class => true, + ServerMaintenanceMode::class => true, ]; private string $appName; @@ -114,17 +116,12 @@ class ExceptionLoggerPlugin extends \Sabre\DAV\ServerPlugin { */ public function logException(\Throwable $ex) { $exceptionClass = get_class($ex); - if (isset($this->nonFatalExceptions[$exceptionClass]) || - ( - $exceptionClass === ServiceUnavailable::class && - $ex->getMessage() === 'System in maintenance mode.' - ) - ) { + if (isset($this->nonFatalExceptions[$exceptionClass])) { $this->logger->debug($ex->getMessage(), [ 'app' => $this->appName, 'exception' => $ex, ]); - return; + return; } $this->logger->critical($ex->getMessage(), [ diff --git a/apps/dav/lib/Connector/Sabre/File.php b/apps/dav/lib/Connector/Sabre/File.php index ebcfdabc6b3..94632b265db 100644 --- a/apps/dav/lib/Connector/Sabre/File.php +++ b/apps/dav/lib/Connector/Sabre/File.php @@ -553,7 +553,7 @@ class File extends Node implements IFile { * @return array|bool */ public function getDirectDownload() { - if (\OCP\App::isEnabled('encryption')) { + if (\OCP\Server::get(\OCP\App\IAppManager::class)->isEnabledForUser('encryption')) { return []; } /** @var \OCP\Files\Storage $storage */ diff --git a/apps/dav/lib/Connector/Sabre/FilesPlugin.php b/apps/dav/lib/Connector/Sabre/FilesPlugin.php index b784764f8fe..e9d27d4e7f6 100644 --- a/apps/dav/lib/Connector/Sabre/FilesPlugin.php +++ b/apps/dav/lib/Connector/Sabre/FilesPlugin.php @@ -65,6 +65,7 @@ class FilesPlugin extends ServerPlugin { public const PERMISSIONS_PROPERTYNAME = '{http://owncloud.org/ns}permissions'; public const SHARE_PERMISSIONS_PROPERTYNAME = '{http://open-collaboration-services.org/ns}share-permissions'; public const OCM_SHARE_PERMISSIONS_PROPERTYNAME = '{http://open-cloud-mesh.org/ns}share-permissions'; + public const SHARE_ATTRIBUTES_PROPERTYNAME = '{http://nextcloud.org/ns}share-attributes'; public const DOWNLOADURL_PROPERTYNAME = '{http://owncloud.org/ns}downloadURL'; public const SIZE_PROPERTYNAME = '{http://owncloud.org/ns}size'; public const GETETAG_PROPERTYNAME = '{DAV:}getetag'; @@ -134,6 +135,7 @@ class FilesPlugin extends ServerPlugin { $server->protectedProperties[] = self::PERMISSIONS_PROPERTYNAME; $server->protectedProperties[] = self::SHARE_PERMISSIONS_PROPERTYNAME; $server->protectedProperties[] = self::OCM_SHARE_PERMISSIONS_PROPERTYNAME; + $server->protectedProperties[] = self::SHARE_ATTRIBUTES_PROPERTYNAME; $server->protectedProperties[] = self::SIZE_PROPERTYNAME; $server->protectedProperties[] = self::DOWNLOADURL_PROPERTYNAME; $server->protectedProperties[] = self::OWNER_ID_PROPERTYNAME; @@ -321,6 +323,10 @@ class FilesPlugin extends ServerPlugin { return json_encode($ocmPermissions); }); + $propFind->handle(self::SHARE_ATTRIBUTES_PROPERTYNAME, function () use ($node, $httpRequest) { + return json_encode($node->getShareAttributes()); + }); + $propFind->handle(self::GETETAG_PROPERTYNAME, function () use ($node): string { return $node->getETag(); }); diff --git a/apps/dav/lib/Connector/Sabre/MaintenancePlugin.php b/apps/dav/lib/Connector/Sabre/MaintenancePlugin.php index e7e3b273b98..1fc02320805 100644 --- a/apps/dav/lib/Connector/Sabre/MaintenancePlugin.php +++ b/apps/dav/lib/Connector/Sabre/MaintenancePlugin.php @@ -27,6 +27,7 @@ */ namespace OCA\DAV\Connector\Sabre; +use OCA\DAV\Exception\ServerMaintenanceMode; use OCP\IConfig; use OCP\IL10N; use OCP\Util; @@ -82,10 +83,10 @@ class MaintenancePlugin extends ServerPlugin { */ public function checkMaintenanceMode() { if ($this->config->getSystemValueBool('maintenance')) { - throw new ServiceUnavailable($this->l10n->t('System is in maintenance mode.')); + throw new ServerMaintenanceMode($this->l10n->t('System is in maintenance mode.')); } if (Util::needUpgrade()) { - throw new ServiceUnavailable($this->l10n->t('Upgrade needed')); + throw new ServerMaintenanceMode($this->l10n->t('Upgrade needed')); } return true; diff --git a/apps/dav/lib/Connector/Sabre/Node.php b/apps/dav/lib/Connector/Sabre/Node.php index e4517068f42..87f2fea394f 100644 --- a/apps/dav/lib/Connector/Sabre/Node.php +++ b/apps/dav/lib/Connector/Sabre/Node.php @@ -38,6 +38,7 @@ namespace OCA\DAV\Connector\Sabre; use OC\Files\Mount\MoveableMount; use OC\Files\Node\File; use OC\Files\Node\Folder; +use OC\Files\Storage\Wrapper\Wrapper; use OC\Files\View; use OCA\DAV\Connector\Sabre\Exception\InvalidPath; use OCP\Files\FileInfo; @@ -323,6 +324,31 @@ abstract class Node implements \Sabre\DAV\INode { } /** + * @return array + */ + public function getShareAttributes(): array { + $attributes = []; + + try { + $storage = $this->info->getStorage(); + } catch (StorageNotAvailableException $e) { + $storage = null; + } + + if ($storage && $storage->instanceOfStorage(\OCA\Files_Sharing\SharedStorage::class)) { + /** @var \OCA\Files_Sharing\SharedStorage $storage */ + $attributes = $storage->getShare()->getAttributes(); + if ($attributes === null) { + return []; + } else { + return $attributes->toArray(); + } + } + + return $attributes; + } + + /** * @param string $user * @return string */ diff --git a/apps/dav/lib/Connector/Sabre/Principal.php b/apps/dav/lib/Connector/Sabre/Principal.php index 94e3978e67d..75bee4e7b42 100644 --- a/apps/dav/lib/Connector/Sabre/Principal.php +++ b/apps/dav/lib/Connector/Sabre/Principal.php @@ -41,6 +41,9 @@ use OC\KnownUser\KnownUserService; use OCA\Circles\Exceptions\CircleNotFoundException; use OCA\DAV\CalDAV\Proxy\ProxyMapper; use OCA\DAV\Traits\PrincipalProxyTrait; +use OCP\Accounts\IAccountManager; +use OCP\Accounts\IAccountProperty; +use OCP\Accounts\PropertyDoesNotExistException; use OCP\App\IAppManager; use OCP\AppFramework\QueryException; use OCP\Constants; @@ -64,6 +67,9 @@ class Principal implements BackendInterface { /** @var IGroupManager */ private $groupManager; + /** @var IAccountManager */ + private $accountManager; + /** @var IShareManager */ private $shareManager; @@ -95,6 +101,7 @@ class Principal implements BackendInterface { public function __construct(IUserManager $userManager, IGroupManager $groupManager, + IAccountManager $accountManager, IShareManager $shareManager, IUserSession $userSession, IAppManager $appManager, @@ -105,6 +112,7 @@ class Principal implements BackendInterface { string $principalPrefix = 'principals/users/') { $this->userManager = $userManager; $this->groupManager = $groupManager; + $this->accountManager = $accountManager; $this->shareManager = $shareManager; $this->userSession = $userSession; $this->appManager = $appManager; @@ -506,6 +514,7 @@ class Principal implements BackendInterface { /** * @param IUser $user * @return array + * @throws PropertyDoesNotExistException */ protected function userToPrincipal($user) { $userId = $user->getUID(); @@ -517,11 +526,18 @@ class Principal implements BackendInterface { '{http://nextcloud.com/ns}language' => $this->languageFactory->getUserLanguage($user), ]; + $account = $this->accountManager->getAccount($user); + $alternativeEmails = array_map(fn (IAccountProperty $property) => 'mailto:' . $property->getValue(), $account->getPropertyCollection(IAccountManager::COLLECTION_EMAIL)->getProperties()); + $email = $user->getSystemEMailAddress(); if (!empty($email)) { $principal['{http://sabredav.org/ns}email-address'] = $email; } + if (!empty($alternativeEmails)) { + $principal['{DAV:}alternate-URI-set'] = $alternativeEmails; + } + return $principal; } diff --git a/apps/dav/lib/Connector/Sabre/ServerFactory.php b/apps/dav/lib/Connector/Sabre/ServerFactory.php index 8f1f710ca5e..4c57f3412e3 100644 --- a/apps/dav/lib/Connector/Sabre/ServerFactory.php +++ b/apps/dav/lib/Connector/Sabre/ServerFactory.php @@ -33,6 +33,7 @@ namespace OCA\DAV\Connector\Sabre; use OCP\Files\Folder; use OCA\DAV\AppInfo\PluginManager; +use OCA\DAV\DAV\ViewOnlyPlugin; use OCA\DAV\Files\BrowserErrorPagePlugin; use OCP\Files\Mount\IMountManager; use OCP\IConfig; @@ -158,6 +159,11 @@ class ServerFactory { $server->addPlugin(new \OCA\DAV\Connector\Sabre\QuotaPlugin($view, true)); $server->addPlugin(new \OCA\DAV\Connector\Sabre\ChecksumUpdatePlugin()); + // Allow view-only plugin for webdav requests + $server->addPlugin(new ViewOnlyPlugin( + $this->logger + )); + if ($this->userSession->isLoggedIn()) { $server->addPlugin(new \OCA\DAV\Connector\Sabre\TagsPlugin($objectTree, $this->tagManager)); $server->addPlugin(new \OCA\DAV\Connector\Sabre\SharesPlugin( diff --git a/apps/dav/lib/Connector/Sabre/ShareTypeList.php b/apps/dav/lib/Connector/Sabre/ShareTypeList.php index 6fbae0dee4a..bacbdc99a73 100644 --- a/apps/dav/lib/Connector/Sabre/ShareTypeList.php +++ b/apps/dav/lib/Connector/Sabre/ShareTypeList.php @@ -79,7 +79,7 @@ class ShareTypeList implements Element { } /** - * The xmlSerialize metod is called during xml writing. + * The xmlSerialize method is called during xml writing. * * @param Writer $writer * @return void diff --git a/apps/dav/lib/Connector/Sabre/ShareeList.php b/apps/dav/lib/Connector/Sabre/ShareeList.php index db8c011cc45..e43f552a8cc 100644 --- a/apps/dav/lib/Connector/Sabre/ShareeList.php +++ b/apps/dav/lib/Connector/Sabre/ShareeList.php @@ -45,7 +45,7 @@ class ShareeList implements XmlSerializable { } /** - * The xmlSerialize metod is called during xml writing. + * The xmlSerialize method is called during xml writing. * * @param Writer $writer * @return void diff --git a/apps/dav/lib/Connector/Sabre/TagList.php b/apps/dav/lib/Connector/Sabre/TagList.php index bbb938fb27d..86006cd3404 100644 --- a/apps/dav/lib/Connector/Sabre/TagList.php +++ b/apps/dav/lib/Connector/Sabre/TagList.php @@ -95,7 +95,7 @@ class TagList implements Element { } /** - * The xmlSerialize metod is called during xml writing. + * The xmlSerialize method is called during xml writing. * * Use the $writer argument to write its own xml serialization. * diff --git a/apps/dav/lib/Controller/DirectController.php b/apps/dav/lib/Controller/DirectController.php index 955400998cf..f9c83488935 100644 --- a/apps/dav/lib/Controller/DirectController.php +++ b/apps/dav/lib/Controller/DirectController.php @@ -31,8 +31,12 @@ use OCA\DAV\Db\DirectMapper; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\OCS\OCSBadRequestException; use OCP\AppFramework\OCS\OCSNotFoundException; +use OCP\AppFramework\OCS\OCSForbiddenException; use OCP\AppFramework\OCSController; use OCP\AppFramework\Utility\ITimeFactory; +use OCP\EventDispatcher\GenericEvent; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\Files\Events\BeforeDirectFileDownloadEvent; use OCP\Files\File; use OCP\Files\IRootFolder; use OCP\IRequest; @@ -59,6 +63,8 @@ class DirectController extends OCSController { /** @var IURLGenerator */ private $urlGenerator; + /** @var IEventDispatcher */ + private $eventDispatcher; public function __construct(string $appName, IRequest $request, @@ -67,7 +73,8 @@ class DirectController extends OCSController { DirectMapper $mapper, ISecureRandom $random, ITimeFactory $timeFactory, - IURLGenerator $urlGenerator) { + IURLGenerator $urlGenerator, + IEventDispatcher $eventDispatcher) { parent::__construct($appName, $request); $this->rootFolder = $rootFolder; @@ -76,6 +83,7 @@ class DirectController extends OCSController { $this->random = $random; $this->timeFactory = $timeFactory; $this->urlGenerator = $urlGenerator; + $this->eventDispatcher = $eventDispatcher; } /** @@ -99,6 +107,13 @@ class DirectController extends OCSController { throw new OCSBadRequestException('Direct download only works for files'); } + $event = new BeforeDirectFileDownloadEvent($userFolder->getRelativePath($file->getPath())); + $this->eventDispatcher->dispatchTyped($event); + + if ($event->isSuccessful() === false) { + throw new OCSForbiddenException('Permission denied to download file'); + } + //TODO: at some point we should use the directdownlaod function of storages $direct = new Direct(); $direct->setUserId($this->userId); diff --git a/apps/dav/lib/DAV/CustomPropertiesBackend.php b/apps/dav/lib/DAV/CustomPropertiesBackend.php index acee65cd00d..0110990a408 100644 --- a/apps/dav/lib/DAV/CustomPropertiesBackend.php +++ b/apps/dav/lib/DAV/CustomPropertiesBackend.php @@ -24,13 +24,15 @@ */ namespace OCA\DAV\DAV; -use OCA\DAV\Connector\Sabre\Node; +use Exception; +use OCP\DB\QueryBuilder\IQueryBuilder; use OCP\IDBConnection; use OCP\IUser; use Sabre\DAV\PropertyStorage\Backend\BackendInterface; use Sabre\DAV\PropFind; use Sabre\DAV\PropPatch; use Sabre\DAV\Tree; +use Sabre\DAV\Xml\Property\Complex; use function array_intersect; class CustomPropertiesBackend implements BackendInterface { @@ -39,6 +41,21 @@ class CustomPropertiesBackend implements BackendInterface { private const TABLE_NAME = 'properties'; /** + * Value is stored as string. + */ + public const PROPERTY_TYPE_STRING = 1; + + /** + * Value is stored as XML fragment. + */ + public const PROPERTY_TYPE_XML = 2; + + /** + * Value is stored as a property object. + */ + public const PROPERTY_TYPE_OBJECT = 3; + + /** * Ignored properties * * @var string[] @@ -239,7 +256,7 @@ class CustomPropertiesBackend implements BackendInterface { $result = $qb->executeQuery(); $props = []; while ($row = $result->fetch()) { - $props[$row['propertyname']] = $row['propertyvalue']; + $props[$row['propertyname']] = $this->decodeValueFromDatabase($row['propertyvalue'], $row['valuetype']); } $result->closeCursor(); return $props; @@ -282,7 +299,7 @@ class CustomPropertiesBackend implements BackendInterface { $props = []; while ($row = $result->fetch()) { - $props[$row['propertyname']] = $row['propertyvalue']; + $props[$row['propertyname']] = $this->decodeValueFromDatabase($row['propertyvalue'], $row['valuetype']); } $result->closeCursor(); @@ -292,68 +309,54 @@ class CustomPropertiesBackend implements BackendInterface { } /** - * Update properties - * - * @param string $path path for which to update properties - * @param array $properties array of properties to update - * - * @return bool + * @throws Exception */ - private function updateProperties(string $path, array $properties) { - $deleteStatement = 'DELETE FROM `*PREFIX*properties`' . - ' WHERE `userid` = ? AND `propertypath` = ? AND `propertyname` = ?'; - - $insertStatement = 'INSERT INTO `*PREFIX*properties`' . - ' (`userid`,`propertypath`,`propertyname`,`propertyvalue`) VALUES(?,?,?,?)'; - - $updateStatement = 'UPDATE `*PREFIX*properties` SET `propertyvalue` = ?' . - ' WHERE `userid` = ? AND `propertypath` = ? AND `propertyname` = ?'; - + private function updateProperties(string $path, array $properties): bool { // TODO: use "insert or update" strategy ? $existing = $this->getUserProperties($path, []); - $this->connection->beginTransaction(); - foreach ($properties as $propertyName => $propertyValue) { - // If it was null, we need to delete the property - if (is_null($propertyValue)) { - if (array_key_exists($propertyName, $existing)) { - $this->connection->executeUpdate($deleteStatement, - [ - $this->user->getUID(), - $this->formatPath($path), - $propertyName, - ] - ); - } - } else { - if ($propertyValue instanceOf \Sabre\DAV\Xml\Property\Complex) { - $propertyValue = $propertyValue->getXml(); - } elseif (!is_string($propertyValue)) { - $propertyValue = (string)$propertyValue; - } - if (!array_key_exists($propertyName, $existing)) { - $this->connection->executeUpdate($insertStatement, - [ - $this->user->getUID(), - $this->formatPath($path), - $propertyName, - $propertyValue, - ] - ); + try { + $this->connection->beginTransaction(); + foreach ($properties as $propertyName => $propertyValue) { + // common parameters for all queries + $dbParameters = [ + 'userid' => $this->user->getUID(), + 'propertyPath' => $this->formatPath($path), + 'propertyName' => $propertyName + ]; + + // If it was null, we need to delete the property + if (is_null($propertyValue)) { + if (array_key_exists($propertyName, $existing)) { + $deleteQuery = $deleteQuery ?? $this->createDeleteQuery(); + $deleteQuery + ->setParameters($dbParameters) + ->executeStatement(); + } } else { - $this->connection->executeUpdate($updateStatement, - [ - $propertyValue, - $this->user->getUID(), - $this->formatPath($path), - $propertyName, - ] - ); + [$value, $valueType] = $this->encodeValueForDatabase($propertyValue); + $dbParameters['propertyValue'] = $value; + $dbParameters['valueType'] = $valueType; + + if (!array_key_exists($propertyName, $existing)) { + $insertQuery = $insertQuery ?? $this->createInsertQuery(); + $insertQuery + ->setParameters($dbParameters) + ->executeStatement(); + } else { + $updateQuery = $updateQuery ?? $this->createUpdateQuery(); + $updateQuery + ->setParameters($dbParameters) + ->executeStatement(); + } } } - } - $this->connection->commit(); - unset($this->userCache[$path]); + $this->connection->commit(); + unset($this->userCache[$path]); + } catch (Exception $e) { + $this->connection->rollBack(); + throw $e; + } return true; } @@ -367,8 +370,73 @@ class CustomPropertiesBackend implements BackendInterface { private function formatPath(string $path): string { if (strlen($path) > 250) { return sha1($path); + } + + return $path; + } + + /** + * @param mixed $value + * @return array + */ + private function encodeValueForDatabase($value): array { + if (is_scalar($value)) { + $valueType = self::PROPERTY_TYPE_STRING; + } elseif ($value instanceof Complex) { + $valueType = self::PROPERTY_TYPE_XML; + $value = $value->getXml(); } else { - return $path; + $valueType = self::PROPERTY_TYPE_OBJECT; + $value = serialize($value); + } + return [$value, $valueType]; + } + + /** + * @return mixed|Complex|string + */ + private function decodeValueFromDatabase(string $value, int $valueType) { + switch ($valueType) { + case self::PROPERTY_TYPE_XML: + return new Complex($value); + case self::PROPERTY_TYPE_OBJECT: + return unserialize($value); + case self::PROPERTY_TYPE_STRING: + default: + return $value; } } + + private function createDeleteQuery(): IQueryBuilder { + $deleteQuery = $this->connection->getQueryBuilder(); + $deleteQuery->delete('properties') + ->where($deleteQuery->expr()->eq('userid', $deleteQuery->createParameter('userid'))) + ->andWhere($deleteQuery->expr()->eq('propertypath', $deleteQuery->createParameter('propertyPath'))) + ->andWhere($deleteQuery->expr()->eq('propertyname', $deleteQuery->createParameter('propertyName'))); + return $deleteQuery; + } + + private function createInsertQuery(): IQueryBuilder { + $insertQuery = $this->connection->getQueryBuilder(); + $insertQuery->insert('properties') + ->values([ + 'userid' => $insertQuery->createParameter('userid'), + 'propertypath' => $insertQuery->createParameter('propertyPath'), + 'propertyname' => $insertQuery->createParameter('propertyName'), + 'propertyvalue' => $insertQuery->createParameter('propertyValue'), + 'valuetype' => $insertQuery->createParameter('valueType'), + ]); + return $insertQuery; + } + + private function createUpdateQuery(): IQueryBuilder { + $updateQuery = $this->connection->getQueryBuilder(); + $updateQuery->update('properties') + ->set('propertyvalue', $updateQuery->createParameter('propertyValue')) + ->set('valuetype', $updateQuery->createParameter('valueType')) + ->where($updateQuery->expr()->eq('userid', $updateQuery->createParameter('userid'))) + ->andWhere($updateQuery->expr()->eq('propertypath', $updateQuery->createParameter('propertyPath'))) + ->andWhere($updateQuery->expr()->eq('propertyname', $updateQuery->createParameter('propertyName'))); + return $updateQuery; + } } diff --git a/apps/dav/lib/DAV/Sharing/Backend.php b/apps/dav/lib/DAV/Sharing/Backend.php index 0f675ea4c15..90d2c7ebf82 100644 --- a/apps/dav/lib/DAV/Sharing/Backend.php +++ b/apps/dav/lib/DAV/Sharing/Backend.php @@ -32,32 +32,20 @@ use OCA\DAV\Connector\Sabre\Principal; use OCP\IDBConnection; use OCP\IGroupManager; use OCP\IUserManager; +use OCP\DB\QueryBuilder\IQueryBuilder; class Backend { - - /** @var IDBConnection */ - private $db; - /** @var IUserManager */ - private $userManager; - /** @var IGroupManager */ - private $groupManager; - /** @var Principal */ - private $principalBackend; - /** @var string */ - private $resourceType; + private IDBConnection $db; + private IUserManager $userManager; + private IGroupManager $groupManager; + private Principal $principalBackend; + private string $resourceType; public const ACCESS_OWNER = 1; public const ACCESS_READ_WRITE = 2; public const ACCESS_READ = 3; - /** - * @param IDBConnection $db - * @param IUserManager $userManager - * @param IGroupManager $groupManager - * @param Principal $principalBackend - * @param string $resourceType - */ - public function __construct(IDBConnection $db, IUserManager $userManager, IGroupManager $groupManager, Principal $principalBackend, $resourceType) { + public function __construct(IDBConnection $db, IUserManager $userManager, IGroupManager $groupManager, Principal $principalBackend, string $resourceType) { $this->db = $db; $this->userManager = $userManager; $this->groupManager = $groupManager; @@ -66,11 +54,10 @@ class Backend { } /** - * @param IShareable $shareable - * @param string[] $add - * @param string[] $remove + * @param list<array{href: string, commonName: string, readOnly: bool}> $add + * @param list<string> $remove */ - public function updateShares(IShareable $shareable, array $add, array $remove) { + public function updateShares(IShareable $shareable, array $add, array $remove): void { foreach ($add as $element) { $principal = $this->principalBackend->findByUri($element['href'], ''); if ($principal !== '') { @@ -86,10 +73,9 @@ class Backend { } /** - * @param IShareable $shareable - * @param string $element + * @param array{href: string, commonName: string, readOnly: bool} $element */ - private function shareWith($shareable, $element) { + private function shareWith(IShareable $shareable, array $element): void { $user = $element['href']; $parts = explode(':', $user, 2); if ($parts[0] !== 'principal') { @@ -129,33 +115,26 @@ class Backend { 'access' => $query->createNamedParameter($access), 'resourceid' => $query->createNamedParameter($shareable->getResourceId()) ]); - $query->execute(); + $query->executeStatement(); } - /** - * @param $resourceId - */ - public function deleteAllShares($resourceId) { + public function deleteAllShares(int $resourceId): void { $query = $this->db->getQueryBuilder(); $query->delete('dav_shares') ->where($query->expr()->eq('resourceid', $query->createNamedParameter($resourceId))) ->andWhere($query->expr()->eq('type', $query->createNamedParameter($this->resourceType))) - ->execute(); + ->executeStatement(); } - public function deleteAllSharesByUser($principaluri) { + public function deleteAllSharesByUser(string $principaluri): void { $query = $this->db->getQueryBuilder(); $query->delete('dav_shares') ->where($query->expr()->eq('principaluri', $query->createNamedParameter($principaluri))) ->andWhere($query->expr()->eq('type', $query->createNamedParameter($this->resourceType))) - ->execute(); + ->executeStatement(); } - /** - * @param IShareable $shareable - * @param string $element - */ - private function unshare($shareable, $element) { + private function unshare(IShareable $shareable, string $element): void { $parts = explode(':', $element, 2); if ($parts[0] !== 'principal') { return; @@ -172,7 +151,7 @@ class Backend { ->andWhere($query->expr()->eq('type', $query->createNamedParameter($this->resourceType))) ->andWhere($query->expr()->eq('principaluri', $query->createNamedParameter($parts[1]))) ; - $query->execute(); + $query->executeStatement(); } /** @@ -183,29 +162,28 @@ class Backend { * * commonName - Optional, for example a first + last name * * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants. * * readOnly - boolean - * * summary - Optional, a description for the share * * @param int $resourceId - * @return array + * @return list<array{href: string, commonName: string, status: int, readOnly: bool, '{http://owncloud.org/ns}principal': string, '{http://owncloud.org/ns}group-share': bool}> */ - public function getShares($resourceId) { + public function getShares(int $resourceId): array { $query = $this->db->getQueryBuilder(); $result = $query->select(['principaluri', 'access']) ->from('dav_shares') - ->where($query->expr()->eq('resourceid', $query->createNamedParameter($resourceId))) + ->where($query->expr()->eq('resourceid', $query->createNamedParameter($resourceId, IQueryBuilder::PARAM_INT))) ->andWhere($query->expr()->eq('type', $query->createNamedParameter($this->resourceType))) ->groupBy(['principaluri', 'access']) - ->execute(); + ->executeQuery(); $shares = []; while ($row = $result->fetch()) { $p = $this->principalBackend->getPrincipalByPath($row['principaluri']); $shares[] = [ - 'href' => "principal:${row['principaluri']}", - 'commonName' => isset($p['{DAV:}displayname']) ? $p['{DAV:}displayname'] : '', + 'href' => "principal:{$row['principaluri']}", + 'commonName' => isset($p['{DAV:}displayname']) ? (string)$p['{DAV:}displayname'] : '', 'status' => 1, 'readOnly' => (int) $row['access'] === self::ACCESS_READ, - '{http://owncloud.org/ns}principal' => $row['principaluri'], + '{http://owncloud.org/ns}principal' => (string)$row['principaluri'], '{http://owncloud.org/ns}group-share' => is_null($p) ]; } @@ -217,10 +195,10 @@ class Backend { * For shared resources the sharee is set in the ACL of the resource * * @param int $resourceId - * @param array $acl - * @return array + * @param list<array{privilege: string, principal: string, protected: bool}> $acl + * @return list<array{privilege: string, principal: string, protected: bool}> */ - public function applyShareAcl($resourceId, $acl) { + public function applyShareAcl(int $resourceId, array $acl): array { $shares = $this->getShares($resourceId); foreach ($shares as $share) { $acl[] = [ diff --git a/apps/dav/lib/DAV/Sharing/IShareable.php b/apps/dav/lib/DAV/Sharing/IShareable.php index 3833e026696..759981af078 100644 --- a/apps/dav/lib/DAV/Sharing/IShareable.php +++ b/apps/dav/lib/DAV/Sharing/IShareable.php @@ -40,16 +40,14 @@ interface IShareable extends INode { * Every element in the add array has the following properties: * * href - A url. Usually a mailto: address * * commonName - Usually a first and last name, or false - * * summary - A description of the share, can also be false * * readOnly - A boolean value * * Every element in the remove array is just the address string. * - * @param array $add - * @param array $remove - * @return void + * @param list<array{href: string, commonName: string, readOnly: bool}> $add + * @param list<string> $remove */ - public function updateShares(array $add, array $remove); + public function updateShares(array $add, array $remove): void; /** * Returns the list of people whom this resource is shared with. @@ -59,19 +57,15 @@ interface IShareable extends INode { * * commonName - Optional, for example a first + last name * * status - See the Sabre\CalDAV\SharingPlugin::STATUS_ constants. * * readOnly - boolean - * * summary - Optional, a description for the share * - * @return array + * @return list<array{href: string, commonName: string, status: int, readOnly: bool, '{http://owncloud.org/ns}principal': string, '{http://owncloud.org/ns}group-share': bool}> */ - public function getShares(); + public function getShares(): array; - /** - * @return int - */ - public function getResourceId(); + public function getResourceId(): int; /** - * @return string + * @return ?string */ public function getOwner(); } diff --git a/apps/dav/lib/DAV/Sharing/Xml/Invite.php b/apps/dav/lib/DAV/Sharing/Xml/Invite.php index 161a8dd0ebf..e6219f2bbfe 100644 --- a/apps/dav/lib/DAV/Sharing/Xml/Invite.php +++ b/apps/dav/lib/DAV/Sharing/Xml/Invite.php @@ -100,7 +100,7 @@ class Invite implements XmlSerializable { } /** - * The xmlSerialize metod is called during xml writing. + * The xmlSerialize method is called during xml writing. * * Use the $writer argument to write its own xml serialization. * diff --git a/apps/dav/lib/DAV/SystemPrincipalBackend.php b/apps/dav/lib/DAV/SystemPrincipalBackend.php index e5b9a20037f..07431feb6dd 100644 --- a/apps/dav/lib/DAV/SystemPrincipalBackend.php +++ b/apps/dav/lib/DAV/SystemPrincipalBackend.php @@ -87,7 +87,7 @@ class SystemPrincipalBackend extends AbstractBackend { } /** - * Updates one ore more webdav properties on a principal. + * Updates one or more webdav properties on a principal. * * The list of mutations is stored in a Sabre\DAV\PropPatch object. * To do the actual updates, you must tell this object which properties diff --git a/apps/dav/lib/DAV/ViewOnlyPlugin.php b/apps/dav/lib/DAV/ViewOnlyPlugin.php new file mode 100644 index 00000000000..1504969b5b4 --- /dev/null +++ b/apps/dav/lib/DAV/ViewOnlyPlugin.php @@ -0,0 +1,108 @@ +<?php +/** + * @author Piotr Mrowczynski piotr@owncloud.com + * + * @copyright Copyright (c) 2019, ownCloud GmbH + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ + +namespace OCA\DAV\DAV; + +use OCA\DAV\Connector\Sabre\Exception\Forbidden; +use OCA\DAV\Connector\Sabre\File as DavFile; +use OCA\DAV\Meta\MetaFile; +use OCP\Files\FileInfo; +use OCP\Files\NotFoundException; +use Psr\Log\LoggerInterface; +use Sabre\DAV\Server; +use Sabre\DAV\ServerPlugin; +use Sabre\HTTP\RequestInterface; +use Sabre\DAV\Exception\NotFound; + +/** + * Sabre plugin for restricting file share receiver download: + */ +class ViewOnlyPlugin extends ServerPlugin { + + private ?Server $server = null; + private LoggerInterface $logger; + + public function __construct(LoggerInterface $logger) { + $this->logger = $logger; + } + + /** + * This initializes the plugin. + * + * This function is called by Sabre\DAV\Server, after + * addPlugin is called. + * + * This method should set up the required event subscriptions. + */ + public function initialize(Server $server): void { + $this->server = $server; + //priority 90 to make sure the plugin is called before + //Sabre\DAV\CorePlugin::httpGet + $this->server->on('method:GET', [$this, 'checkViewOnly'], 90); + } + + /** + * Disallow download via DAV Api in case file being received share + * and having special permission + * + * @throws Forbidden + * @throws NotFoundException + */ + public function checkViewOnly(RequestInterface $request): bool { + $path = $request->getPath(); + + try { + assert($this->server !== null); + $davNode = $this->server->tree->getNodeForPath($path); + if (!($davNode instanceof DavFile)) { + return true; + } + // Restrict view-only to nodes which are shared + $node = $davNode->getNode(); + + $storage = $node->getStorage(); + + if (!$storage->instanceOfStorage(\OCA\Files_Sharing\SharedStorage::class)) { + return true; + } + // Extract extra permissions + /** @var \OCA\Files_Sharing\SharedStorage $storage */ + $share = $storage->getShare(); + + $attributes = $share->getAttributes(); + if ($attributes === null) { + return true; + } + + // Check if read-only and on whether permission can download is both set and disabled. + $canDownload = $attributes->getAttribute('permissions', 'download'); + if ($canDownload !== null && !$canDownload) { + throw new Forbidden('Access to this resource has been denied because it is in view-only mode.'); + } + } catch (NotFound $e) { + $this->logger->warning($e->getMessage(), [ + 'exception' => $e, + ]); + } + + return true; + } +} diff --git a/apps/dav/lib/Direct/DirectHome.php b/apps/dav/lib/Direct/DirectHome.php index a385cd8f39d..5453a61ed46 100644 --- a/apps/dav/lib/Direct/DirectHome.php +++ b/apps/dav/lib/Direct/DirectHome.php @@ -91,7 +91,7 @@ class DirectHome implements ICollection { return new DirectFile($direct, $this->rootFolder, $this->eventDispatcher); } catch (DoesNotExistException $e) { - // Since the token space is so huge only throttle on non exsisting token + // Since the token space is so huge only throttle on non-existing token $this->throttler->registerAttempt('directlink', $this->request->getRemoteAddress()); $this->throttler->sleepDelay($this->request->getRemoteAddress(), 'directlink'); diff --git a/apps/dav/lib/Events/CalendarObjectMovedEvent.php b/apps/dav/lib/Events/CalendarObjectMovedEvent.php new file mode 100644 index 00000000000..0143dad9a96 --- /dev/null +++ b/apps/dav/lib/Events/CalendarObjectMovedEvent.php @@ -0,0 +1,120 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright Copyright (c) 2020, Georg Ehrke + * + * @author Georg Ehrke <oc.list@georgehrke.com> + * + * @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\Events; + +use OCP\EventDispatcher\Event; + +/** + * Class CalendarObjectMovedEvent + * + * @package OCA\DAV\Events + * @since 25.0.0 + */ +class CalendarObjectMovedEvent extends Event { + private int $sourceCalendarId; + private array $sourceCalendarData; + private int $targetCalendarId; + private array $targetCalendarData; + private array $sourceShares; + private array $targetShares; + private array $objectData; + + /** + * @since 25.0.0 + */ + public function __construct(int $sourceCalendarId, + array $sourceCalendarData, + int $targetCalendarId, + array $targetCalendarData, + array $sourceShares, + array $targetShares, + array $objectData) { + parent::__construct(); + $this->sourceCalendarId = $sourceCalendarId; + $this->sourceCalendarData = $sourceCalendarData; + $this->targetCalendarId = $targetCalendarId; + $this->targetCalendarData = $targetCalendarData; + $this->sourceShares = $sourceShares; + $this->targetShares = $targetShares; + $this->objectData = $objectData; + } + + /** + * @return int + * @since 25.0.0 + */ + public function getSourceCalendarId(): int { + return $this->sourceCalendarId; + } + + /** + * @return array + * @since 25.0.0 + */ + public function getSourceCalendarData(): array { + return $this->sourceCalendarData; + } + + /** + * @return int + * @since 25.0.0 + */ + public function getTargetCalendarId(): int { + return $this->targetCalendarId; + } + + /** + * @return array + * @since 25.0.0 + */ + public function getTargetCalendarData(): array { + return $this->targetCalendarData; + } + + /** + * @return array + * @since 25.0.0 + */ + public function getSourceShares(): array { + return $this->sourceShares; + } + + /** + * @return array + * @since 25.0.0 + */ + public function getTargetShares(): array { + return $this->targetShares; + } + + /** + * @return array + * @since 25.0.0 + */ + public function getObjectData(): array { + return $this->objectData; + } +} diff --git a/apps/dav/lib/Events/CalendarPublishedEvent.php b/apps/dav/lib/Events/CalendarPublishedEvent.php index 7b3b95f2f77..a95e9f294c1 100644 --- a/apps/dav/lib/Events/CalendarPublishedEvent.php +++ b/apps/dav/lib/Events/CalendarPublishedEvent.php @@ -6,6 +6,7 @@ declare(strict_types=1); * @copyright Copyright (c) 2020, Georg Ehrke * * @author Georg Ehrke <oc.list@georgehrke.com> + * @author Thomas Citharel <nextcloud@tcit.fr> * * @license GNU AGPL version 3 or any later version * @@ -34,15 +35,9 @@ use OCP\EventDispatcher\Event; * @since 20.0.0 */ class CalendarPublishedEvent extends Event { - - /** @var int */ - private $calendarId; - - /** @var array */ - private $calendarData; - - /** @var string */ - private $publicUri; + private int $calendarId; + private array $calendarData; + private string $publicUri; /** * CalendarPublishedEvent constructor. diff --git a/apps/dav/lib/Events/CalendarShareUpdatedEvent.php b/apps/dav/lib/Events/CalendarShareUpdatedEvent.php index a9011bc0273..d5a568d149b 100644 --- a/apps/dav/lib/Events/CalendarShareUpdatedEvent.php +++ b/apps/dav/lib/Events/CalendarShareUpdatedEvent.php @@ -26,6 +26,8 @@ declare(strict_types=1); namespace OCA\DAV\Events; use OCP\EventDispatcher\Event; +use Sabre\CalDAV\Xml\Property\ScheduleCalendarTransp; +use Sabre\CalDAV\Xml\Property\SupportedCalendarComponentSet; /** * Class CalendarShareUpdatedEvent @@ -34,30 +36,28 @@ use OCP\EventDispatcher\Event; * @since 20.0.0 */ class CalendarShareUpdatedEvent extends Event { + private int $calendarId; - /** @var int */ - private $calendarId; + /** @var array{id: int, uri: string, '{http://calendarserver.org/ns/}getctag': string, '{http://sabredav.org/ns}sync-token': int, '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set': SupportedCalendarComponentSet, '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp': ScheduleCalendarTransp } */ + private array $calendarData; - /** @var array */ - private $calendarData; + /** @var list<array{href: string, commonName: string, status: int, readOnly: bool, '{http://owncloud.org/ns}principal': string, '{http://owncloud.org/ns}group-share': bool}> */ + private array $oldShares; - /** @var array */ - private $oldShares; + /** @var list<array{href: string, commonName: string, readOnly: bool}> */ + private array $added; - /** @var array */ - private $added; - - /** @var array */ - private $removed; + /** @var list<string> */ + private array $removed; /** * CalendarShareUpdatedEvent constructor. * * @param int $calendarId - * @param array $calendarData - * @param array $oldShares - * @param array $added - * @param array $removed + * @param array{id: int, uri: string, '{http://calendarserver.org/ns/}getctag': string, '{http://sabredav.org/ns}sync-token': int, '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set': SupportedCalendarComponentSet, '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp': ScheduleCalendarTransp } $calendarData + * @param list<array{href: string, commonName: string, status: int, readOnly: bool, '{http://owncloud.org/ns}principal': string, '{http://owncloud.org/ns}group-share': bool}> $oldShares + * @param list<array{href: string, commonName: string, readOnly: bool}> $added + * @param list<string> $removed * @since 20.0.0 */ public function __construct(int $calendarId, @@ -74,7 +74,6 @@ class CalendarShareUpdatedEvent extends Event { } /** - * @return int * @since 20.0.0 */ public function getCalendarId(): int { @@ -82,7 +81,7 @@ class CalendarShareUpdatedEvent extends Event { } /** - * @return array + * @return array{id: int, uri: string, '{http://calendarserver.org/ns/}getctag': string, '{http://sabredav.org/ns}sync-token': int, '{urn:ietf:params:xml:ns:caldav}supported-calendar-component-set': SupportedCalendarComponentSet, '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp': ScheduleCalendarTransp } * @since 20.0.0 */ public function getCalendarData(): array { @@ -90,7 +89,7 @@ class CalendarShareUpdatedEvent extends Event { } /** - * @return array + * @return list<array{href: string, commonName: string, status: int, readOnly: bool, '{http://owncloud.org/ns}principal': string, '{http://owncloud.org/ns}group-share': bool}> * @since 20.0.0 */ public function getOldShares(): array { @@ -98,7 +97,7 @@ class CalendarShareUpdatedEvent extends Event { } /** - * @return array + * @return list<array{href: string, commonName: string, readOnly: bool}> * @since 20.0.0 */ public function getAdded(): array { @@ -106,7 +105,7 @@ class CalendarShareUpdatedEvent extends Event { } /** - * @return array + * @return list<string> * @since 20.0.0 */ public function getRemoved(): array { diff --git a/apps/dav/lib/Events/CalendarUnpublishedEvent.php b/apps/dav/lib/Events/CalendarUnpublishedEvent.php index 0cea53c6f0d..b2536fc7aef 100644 --- a/apps/dav/lib/Events/CalendarUnpublishedEvent.php +++ b/apps/dav/lib/Events/CalendarUnpublishedEvent.php @@ -6,6 +6,7 @@ declare(strict_types=1); * @copyright Copyright (c) 2020, Georg Ehrke * * @author Georg Ehrke <oc.list@georgehrke.com> + * @author Thomas Citharel <nextcloud@tcit.fr> * * @license GNU AGPL version 3 or any later version * @@ -34,12 +35,8 @@ use OCP\EventDispatcher\Event; * @since 20.0.0 */ class CalendarUnpublishedEvent extends Event { - - /** @var int */ - private $calendarId; - - /** @var array */ - private $calendarData; + private int $calendarId; + private array $calendarData; /** * CalendarUnpublishedEvent constructor. diff --git a/apps/dav/lib/Exception/ServerMaintenanceMode.php b/apps/dav/lib/Exception/ServerMaintenanceMode.php new file mode 100644 index 00000000000..9dad9f2d4d1 --- /dev/null +++ b/apps/dav/lib/Exception/ServerMaintenanceMode.php @@ -0,0 +1,31 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com> + * + * @author Joas Schilling <coding@schilljs.com> + * + * @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\Exception; + +use Sabre\DAV\Exception\ServiceUnavailable; + +class ServerMaintenanceMode extends ServiceUnavailable { + +} diff --git a/apps/dav/lib/Listener/ActivityUpdaterListener.php b/apps/dav/lib/Listener/ActivityUpdaterListener.php index 371912ff035..ea3ec49c14d 100644 --- a/apps/dav/lib/Listener/ActivityUpdaterListener.php +++ b/apps/dav/lib/Listener/ActivityUpdaterListener.php @@ -32,6 +32,7 @@ use OCA\DAV\Events\CalendarDeletedEvent; use OCA\DAV\Events\CalendarMovedToTrashEvent; use OCA\DAV\Events\CalendarObjectCreatedEvent; use OCA\DAV\Events\CalendarObjectDeletedEvent; +use OCA\DAV\Events\CalendarObjectMovedEvent; use OCA\DAV\Events\CalendarObjectMovedToTrashEvent; use OCA\DAV\Events\CalendarObjectRestoredEvent; use OCA\DAV\Events\CalendarObjectUpdatedEvent; @@ -173,7 +174,26 @@ class ActivityUpdaterListener implements IEventListener { ); $this->logger->debug( - sprintf('Activity generated for deleted calendar object %d', $event->getCalendarId()) + sprintf('Activity generated for updated calendar object in calendar %d', $event->getCalendarId()) + ); + } catch (Throwable $e) { + // Any error with activities shouldn't abort the calendar deletion, so we just log it + $this->logger->error('Error generating activity for a deleted calendar object: ' . $e->getMessage(), [ + 'exception' => $e, + ]); + } + } elseif ($event instanceof CalendarObjectMovedEvent) { + try { + $this->activityBackend->onMovedCalendarObject( + $event->getSourceCalendarData(), + $event->getTargetCalendarData(), + $event->getSourceShares(), + $event->getTargetShares(), + $event->getObjectData() + ); + + $this->logger->debug( + sprintf('Activity generated for moved calendar object from calendar %d to calendar %d', $event->getSourceCalendarId(), $event->getTargetCalendarId()) ); } catch (Throwable $e) { // Any error with activities shouldn't abort the calendar deletion, so we just log it diff --git a/apps/dav/lib/Listener/BirthdayListener.php b/apps/dav/lib/Listener/BirthdayListener.php new file mode 100644 index 00000000000..43ad782fa9e --- /dev/null +++ b/apps/dav/lib/Listener/BirthdayListener.php @@ -0,0 +1,54 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2022 Thomas Citharel <nextcloud@tcit.fr> + * + * @author Thomas Citharel <nextcloud@tcit.fr> + * + * @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\Listener; + +use OCA\DAV\CalDAV\BirthdayService; +use OCA\DAV\Events\CardCreatedEvent; +use OCA\DAV\Events\CardDeletedEvent; +use OCA\DAV\Events\CardUpdatedEvent; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; + +class BirthdayListener implements IEventListener { + private BirthdayService $birthdayService; + + public function __construct(BirthdayService $birthdayService) { + $this->birthdayService = $birthdayService; + } + + public function handle(Event $event): void { + if ($event instanceof CardCreatedEvent || $event instanceof CardUpdatedEvent) { + $cardData = $event->getCardData(); + + $this->birthdayService->onCardChanged($event->getAddressBookId(), $cardData['uri'], $cardData['carddata']); + } + + if ($event instanceof CardDeletedEvent) { + $cardData = $event->getCardData(); + $this->birthdayService->onCardDeleted($event->getAddressBookId(), $cardData['uri']); + } + } +} diff --git a/apps/dav/lib/Listener/CalendarPublicationListener.php b/apps/dav/lib/Listener/CalendarPublicationListener.php new file mode 100644 index 00000000000..1453694d6fb --- /dev/null +++ b/apps/dav/lib/Listener/CalendarPublicationListener.php @@ -0,0 +1,65 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2022 Thomas Citharel <nextcloud@tcit.fr> + * + * @author Thomas Citharel <nextcloud@tcit.fr> + * + * @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\Listener; + +use OCA\DAV\CalDAV\Activity\Backend; +use OCA\DAV\Events\CalendarPublishedEvent; +use OCA\DAV\Events\CalendarUnpublishedEvent; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use Psr\Log\LoggerInterface; + +class CalendarPublicationListener implements IEventListener { + private Backend $activityBackend; + private LoggerInterface $logger; + + public function __construct(Backend $activityBackend, + LoggerInterface $logger) { + $this->activityBackend = $activityBackend; + $this->logger = $logger; + } + + /** + * In case the user has set their default calendar to the deleted one + */ + public function handle(Event $event): void { + if ($event instanceof CalendarPublishedEvent) { + $this->logger->debug('Creating activity for Calendar being published'); + + $this->activityBackend->onCalendarPublication( + $event->getCalendarData(), + true + ); + } elseif ($event instanceof CalendarUnpublishedEvent) { + $this->logger->debug('Creating activity for Calendar being unpublished'); + + $this->activityBackend->onCalendarPublication( + $event->getCalendarData(), + false + ); + } + } +} diff --git a/apps/dav/lib/Listener/CalendarShareUpdateListener.php b/apps/dav/lib/Listener/CalendarShareUpdateListener.php new file mode 100644 index 00000000000..88865759162 --- /dev/null +++ b/apps/dav/lib/Listener/CalendarShareUpdateListener.php @@ -0,0 +1,62 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2022 Thomas Citharel <nextcloud@tcit.fr> + * + * @author Thomas Citharel <nextcloud@tcit.fr> + * + * @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\Listener; + +use OCA\DAV\CalDAV\Activity\Backend; +use OCA\DAV\Events\CalendarShareUpdatedEvent; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use Psr\Log\LoggerInterface; + +class CalendarShareUpdateListener implements IEventListener { + private Backend $activityBackend; + private LoggerInterface $logger; + + public function __construct(Backend $activityBackend, + LoggerInterface $logger) { + $this->activityBackend = $activityBackend; + $this->logger = $logger; + } + + /** + * In case the user has set their default calendar to the deleted one + */ + public function handle(Event $event): void { + if (!($event instanceof CalendarShareUpdatedEvent)) { + // Not what we subscribed to + return; + } + + $this->logger->debug("Creating activity for Calendar having it's shares updated"); + + $this->activityBackend->onCalendarUpdateShares( + $event->getCalendarData(), + $event->getOldShares(), + $event->getAdded(), + $event->getRemoved() + ); + } +} diff --git a/apps/dav/lib/Listener/ClearPhotoCacheListener.php b/apps/dav/lib/Listener/ClearPhotoCacheListener.php new file mode 100644 index 00000000000..ed02770e35d --- /dev/null +++ b/apps/dav/lib/Listener/ClearPhotoCacheListener.php @@ -0,0 +1,48 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2022 Thomas Citharel <nextcloud@tcit.fr> + * + * @author Thomas Citharel <nextcloud@tcit.fr> + * + * @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\Listener; + +use OCA\DAV\CardDAV\PhotoCache; +use OCA\DAV\Events\CardDeletedEvent; +use OCA\DAV\Events\CardUpdatedEvent; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; + +class ClearPhotoCacheListener implements IEventListener { + private PhotoCache $photoCache; + + public function __construct(PhotoCache $photoCache) { + $this->photoCache = $photoCache; + } + + public function handle(Event $event): void { + if ($event instanceof CardUpdatedEvent || $event instanceof CardDeletedEvent) { + $cardData = $event->getCardData(); + + $this->photoCache->delete($event->getAddressBookId(), $cardData['uri']); + } + } +} diff --git a/apps/dav/lib/Listener/SubscriptionListener.php b/apps/dav/lib/Listener/SubscriptionListener.php new file mode 100644 index 00000000000..36db234aa05 --- /dev/null +++ b/apps/dav/lib/Listener/SubscriptionListener.php @@ -0,0 +1,85 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2022 Thomas Citharel <nextcloud@tcit.fr> + * + * @author Thomas Citharel <nextcloud@tcit.fr> + * + * @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\Listener; + +use OCA\DAV\BackgroundJob\RefreshWebcalJob; +use OCA\DAV\CalDAV\Reminder\Backend as ReminderBackend; +use OCA\DAV\CalDAV\WebcalCaching\RefreshWebcalService; +use OCA\DAV\Events\SubscriptionCreatedEvent; +use OCA\DAV\Events\SubscriptionDeletedEvent; +use OCP\BackgroundJob\IJobList; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use Psr\Log\LoggerInterface; + +class SubscriptionListener implements IEventListener { + private IJobList $jobList; + private RefreshWebcalService $refreshWebcalService; + private ReminderBackend $reminderBackend; + private LoggerInterface $logger; + + public function __construct(IJobList $jobList, RefreshWebcalService $refreshWebcalService, ReminderBackend $reminderBackend, + LoggerInterface $logger) { + $this->jobList = $jobList; + $this->refreshWebcalService = $refreshWebcalService; + $this->reminderBackend = $reminderBackend; + $this->logger = $logger; + } + + /** + * In case the user has set their default calendar to the deleted one + */ + public function handle(Event $event): void { + if ($event instanceof SubscriptionCreatedEvent) { + $subscriptionId = $event->getSubscriptionId(); + $subscriptionData = $event->getSubscriptionData(); + + $this->logger->debug('Refreshing webcal data for subscription ' . $subscriptionId); + $this->refreshWebcalService->refreshSubscription( + (string)$subscriptionData['principaluri'], + (string)$subscriptionData['uri'] + ); + + $this->logger->debug('Scheduling webcal data refreshment for subscription ' . $subscriptionId); + $this->jobList->add(RefreshWebcalJob::class, [ + 'principaluri' => $subscriptionData['principaluri'], + 'uri' => $subscriptionData['uri'] + ]); + } elseif ($event instanceof SubscriptionDeletedEvent) { + $subscriptionId = $event->getSubscriptionId(); + $subscriptionData = $event->getSubscriptionData(); + + $this->logger->debug('Removing refresh webcal job for subscription ' . $subscriptionId); + $this->jobList->remove(RefreshWebcalJob::class, [ + 'principaluri' => $subscriptionData['principaluri'], + 'uri' => $subscriptionData['uri'] + ]); + + $this->logger->debug('Cleaning all reminders for subscription ' . $subscriptionId); + $this->reminderBackend->cleanRemindersForCalendar($subscriptionId); + } + } +} diff --git a/apps/dav/lib/Listener/TrustedServerRemovedListener.php b/apps/dav/lib/Listener/TrustedServerRemovedListener.php new file mode 100644 index 00000000000..29ff050983b --- /dev/null +++ b/apps/dav/lib/Listener/TrustedServerRemovedListener.php @@ -0,0 +1,50 @@ +<?php + +declare(strict_types=1); + +/** + * @copyright 2022 Carl Schwan <carl@carlschwan.eu> + * + * @author Carl Schwan <carl@carlschwan.eu> + * + * @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\Listener; + +use OCA\DAV\CardDAV\CardDavBackend; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\Federation\Events\TrustedServerRemovedEvent; + +class TrustedServerRemovedListener implements IEventListener { + private CardDavBackend $cardDavBackend; + + public function __construct(CardDavBackend $cardDavBackend) { + $this->cardDavBackend = $cardDavBackend; + } + + public function handle(Event $event): void { + if (!$event instanceof TrustedServerRemovedEvent) { + return; + } + $addressBookUri = $event->getUrlHash(); + $addressBook = $this->cardDavBackend->getAddressBooksByUri('principals/system/system', $addressBookUri); + if (!is_null($addressBook)) { + $this->cardDavBackend->deleteAddressBook($addressBook['id']); + } + } +} diff --git a/apps/dav/lib/Listener/UserPreferenceListener.php b/apps/dav/lib/Listener/UserPreferenceListener.php new file mode 100644 index 00000000000..947f6d3fd01 --- /dev/null +++ b/apps/dav/lib/Listener/UserPreferenceListener.php @@ -0,0 +1,59 @@ +<?php + +declare(strict_types=1); +/** + * @copyright Copyright (c) 2022 Joas Schilling <coding@schilljs.com> + * + * @author Joas Schilling <coding@schilljs.com> + * + * @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\Listener; + +use OCA\DAV\BackgroundJob\UserStatusAutomation; +use OCP\BackgroundJob\IJobList; +use OCP\Config\BeforePreferenceDeletedEvent; +use OCP\Config\BeforePreferenceSetEvent; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; + +class UserPreferenceListener implements IEventListener { + + protected IJobList $jobList; + + public function __construct(IJobList $jobList) { + $this->jobList = $jobList; + } + + public function handle(Event $event): void { + if ($event instanceof BeforePreferenceSetEvent) { + if ($event->getAppId() === 'dav' && $event->getConfigKey() === 'user_status_automation' && $event->getConfigValue() === 'yes') { + $event->setValid(true); + + // Not the cleanest way, but we just add the job in the before event. + // If something ever turns wrong the first execution will remove the job again. + // We also first delete the current job, so the next run time is reset. + $this->jobList->remove(UserStatusAutomation::class, ['userId' => $event->getUserId()]); + $this->jobList->add(UserStatusAutomation::class, ['userId' => $event->getUserId()]); + } + } elseif ($event instanceof BeforePreferenceDeletedEvent) { + if ($event->getAppId() === 'dav' && $event->getConfigKey() === 'user_status_automation') { + $event->setValid(true); + } + } + } +} diff --git a/apps/dav/lib/Migration/RemoveObjectProperties.php b/apps/dav/lib/Migration/RemoveObjectProperties.php new file mode 100644 index 00000000000..c72dfbebfea --- /dev/null +++ b/apps/dav/lib/Migration/RemoveObjectProperties.php @@ -0,0 +1,65 @@ +<?php +/** + * @copyright Copyright (c) 2021, Thomas Citharel <nextcloud@tcit.fr>. + * + * @author Thomas Citharel <nextcloud@tcit.fr> + * + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * 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, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ +namespace OCA\DAV\Migration; + +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; +use OCP\Migration\IOutput; +use OCP\Migration\IRepairStep; + +class RemoveObjectProperties implements IRepairStep { + private const RESOURCE_TYPE_PROPERTY = '{DAV:}resourcetype'; + private const ME_CARD_PROPERTY = '{http://calendarserver.org/ns/}me-card'; + private const CALENDAR_TRANSP_PROPERTY = '{urn:ietf:params:xml:ns:caldav}schedule-calendar-transp'; + + /** @var IDBConnection */ + private $connection; + + /** + * RemoveObjectProperties constructor. + * + * @param IDBConnection $connection + */ + public function __construct(IDBConnection $connection) { + $this->connection = $connection; + } + + /** + * @inheritdoc + */ + public function getName() { + return 'Remove invalid object properties'; + } + + /** + * @inheritdoc + */ + public function run(IOutput $output) { + $query = $this->connection->getQueryBuilder(); + $updated = $query->delete('properties') + ->where($query->expr()->in('propertyname', $query->createNamedParameter([self::RESOURCE_TYPE_PROPERTY, self::ME_CARD_PROPERTY, self::CALENDAR_TRANSP_PROPERTY], IQueryBuilder::PARAM_STR_ARRAY))) + ->andWhere($query->expr()->eq('propertyvalue', $query->createNamedParameter('Object'))) + ->executeStatement(); + + $output->info("$updated invalid object properties removed."); + } +} diff --git a/apps/dav/lib/Migration/Version1024Date20211221144219.php b/apps/dav/lib/Migration/Version1024Date20211221144219.php new file mode 100644 index 00000000000..b93f8ac801e --- /dev/null +++ b/apps/dav/lib/Migration/Version1024Date20211221144219.php @@ -0,0 +1,58 @@ +<?php + +declare(strict_types=1); + +namespace OCA\DAV\Migration; + +use Closure; +use Doctrine\DBAL\Schema\SchemaException; +use OCA\DAV\DAV\CustomPropertiesBackend; +use OCP\DB\ISchemaWrapper; +use OCP\DB\Types; +use OCP\Migration\IOutput; +use OCP\Migration\SimpleMigrationStep; + +/** + * Auto-generated migration step: Please modify to your needs! + */ +class Version1024Date20211221144219 extends SimpleMigrationStep { + + /** + * @param IOutput $output + * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + */ + public function preSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { + } + + /** + * @param IOutput $output + * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + * @return null|ISchemaWrapper + * @throws SchemaException + */ + public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper { + /** @var ISchemaWrapper $schema */ + $schema = $schemaClosure(); + $propertiesTable = $schema->getTable('properties'); + + if ($propertiesTable->hasColumn('valuetype')) { + return null; + } + $propertiesTable->addColumn('valuetype', Types::SMALLINT, [ + 'notnull' => false, + 'default' => CustomPropertiesBackend::PROPERTY_TYPE_STRING + ]); + + return $schema; + } + + /** + * @param IOutput $output + * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper` + * @param array $options + */ + public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): void { + } +} diff --git a/apps/dav/lib/RootCollection.php b/apps/dav/lib/RootCollection.php index 8a11a676609..29ab65d46a9 100644 --- a/apps/dav/lib/RootCollection.php +++ b/apps/dav/lib/RootCollection.php @@ -7,6 +7,7 @@ * @author Georg Ehrke <oc.list@georgehrke.com> * @author Joas Schilling <coding@schilljs.com> * @author Roeland Jago Douma <roeland@famdouma.nl> + * @author Thomas Citharel <nextcloud@tcit.fr> * @author Thomas Müller <thomas.mueller@tmit.eu> * @author Vincent Petry <vincent@nextcloud.com> * @@ -43,6 +44,7 @@ use OCA\DAV\DAV\GroupPrincipalBackend; use OCA\DAV\DAV\SystemPrincipalBackend; use OCA\DAV\Provisioning\Apple\AppleProvisioningNode; use OCA\DAV\Upload\CleanupService; +use OCP\Accounts\IAccountManager; use OCP\App\IAppManager; use OCP\AppFramework\Utility\ITimeFactory; use OCP\EventDispatcher\IEventDispatcher; @@ -61,13 +63,13 @@ class RootCollection extends SimpleCollection { $shareManager = \OC::$server->getShareManager(); $db = \OC::$server->getDatabaseConnection(); $dispatcher = \OC::$server->get(IEventDispatcher::class); - $legacyDispatcher = \OC::$server->getEventDispatcher(); $config = \OC::$server->get(IConfig::class); $proxyMapper = \OC::$server->query(ProxyMapper::class); $userPrincipalBackend = new Principal( $userManager, $groupManager, + \OC::$server->get(IAccountManager::class), $shareManager, \OC::$server->getUserSession(), \OC::$server->getAppManager(), @@ -105,7 +107,6 @@ class RootCollection extends SimpleCollection { $random, $logger, $dispatcher, - $legacyDispatcher, $config ); $userCalendarRoot = new CalendarRoot($userPrincipalBackend, $caldavBackend, 'principals/users', $logger); @@ -140,11 +141,11 @@ class RootCollection extends SimpleCollection { ); $pluginManager = new PluginManager(\OC::$server, \OC::$server->query(IAppManager::class)); - $usersCardDavBackend = new CardDavBackend($db, $userPrincipalBackend, $userManager, $groupManager, $dispatcher, $legacyDispatcher); + $usersCardDavBackend = new CardDavBackend($db, $userPrincipalBackend, $userManager, $groupManager, $dispatcher); $usersAddressBookRoot = new AddressBookRoot($userPrincipalBackend, $usersCardDavBackend, $pluginManager, 'principals/users'); $usersAddressBookRoot->disableListing = $disableListing; - $systemCardDavBackend = new CardDavBackend($db, $userPrincipalBackend, $userManager, $groupManager, $dispatcher, $legacyDispatcher); + $systemCardDavBackend = new CardDavBackend($db, $userPrincipalBackend, $userManager, $groupManager, $dispatcher); $systemAddressBookRoot = new AddressBookRoot(new SystemPrincipalBackend(), $systemCardDavBackend, $pluginManager, 'principals/system'); $systemAddressBookRoot->disableListing = $disableListing; diff --git a/apps/dav/lib/Server.php b/apps/dav/lib/Server.php index 5b532465aba..2cfcb3f5393 100644 --- a/apps/dav/lib/Server.php +++ b/apps/dav/lib/Server.php @@ -62,6 +62,7 @@ use OCA\DAV\Connector\Sabre\SharesPlugin; use OCA\DAV\Connector\Sabre\TagsPlugin; use OCA\DAV\DAV\CustomPropertiesBackend; use OCA\DAV\DAV\PublicAuth; +use OCA\DAV\DAV\ViewOnlyPlugin; use OCA\DAV\Events\SabrePluginAuthInitEvent; use OCA\DAV\Files\BrowserErrorPagePlugin; use OCA\DAV\Files\LazySearchBackend; @@ -229,6 +230,11 @@ class Server { $this->server->addPlugin(new FakeLockerPlugin()); } + // Allow view-only plugin for webdav requests + $this->server->addPlugin(new ViewOnlyPlugin( + $logger + )); + if (BrowserErrorPagePlugin::isBrowserRequest($request)) { $this->server->addPlugin(new BrowserErrorPagePlugin()); } diff --git a/apps/dav/lib/Settings/AvailabilitySettings.php b/apps/dav/lib/Settings/AvailabilitySettings.php index 9a163e21edb..d2b75ba4866 100644 --- a/apps/dav/lib/Settings/AvailabilitySettings.php +++ b/apps/dav/lib/Settings/AvailabilitySettings.php @@ -27,10 +27,34 @@ namespace OCA\DAV\Settings; use OCA\DAV\AppInfo\Application; use OCP\AppFramework\Http\TemplateResponse; +use OCP\AppFramework\Services\IInitialState; +use OCP\IConfig; use OCP\Settings\ISettings; class AvailabilitySettings implements ISettings { + protected IConfig $config; + protected IInitialState $initialState; + protected ?string $userId; + + public function __construct(IConfig $config, + IInitialState $initialState, + ?string $userId) { + $this->config = $config; + $this->initialState = $initialState; + $this->userId = $userId; + } + public function getForm(): TemplateResponse { + $this->initialState->provideInitialState( + 'user_status_automation', + $this->config->getUserValue( + $this->userId, + 'dav', + 'user_status_automation', + 'no' + ) + ); + return new TemplateResponse(Application::APP_ID, 'settings-personal-availability'); } diff --git a/apps/dav/lib/UserMigration/ContactsMigrator.php b/apps/dav/lib/UserMigration/ContactsMigrator.php index ae1a61ce4f4..d2ba82eb2e5 100644 --- a/apps/dav/lib/UserMigration/ContactsMigrator.php +++ b/apps/dav/lib/UserMigration/ContactsMigrator.php @@ -131,6 +131,10 @@ class ContactsMigrator implements IMigrator, ISizeEstimationMigrator { } } + if (count($vCards) === 0) { + throw new InvalidAddressBookException(); + } + return [ 'name' => $addressBookNode->getName(), 'displayName' => $addressBookInfo['{DAV:}displayname'], |