From 2496f83463312561c72c75b7e496a999365df764 Mon Sep 17 00:00:00 2001 From: Joas Schilling Date: Fri, 21 May 2021 14:22:50 +0200 Subject: [PATCH] Add activities for addressbook management Signed-off-by: Joas Schilling --- apps/dav/appinfo/info.xml | 1 + .../composer/composer/autoload_classmap.php | 4 + .../dav/composer/composer/autoload_static.php | 4 + apps/dav/lib/AppInfo/Application.php | 11 + apps/dav/lib/CardDAV/Activity/Backend.php | 521 ++++++++++++++++++ .../CardDAV/Activity/Provider/Addressbook.php | 203 +++++++ .../lib/CardDAV/Activity/Provider/Base.php | 159 ++++++ apps/dav/lib/Listener/AddressbookListener.php | 122 ++++ core/img/places/contacts-dark.png | Bin 0 -> 595 bytes 9 files changed, 1025 insertions(+) create mode 100644 apps/dav/lib/CardDAV/Activity/Backend.php create mode 100644 apps/dav/lib/CardDAV/Activity/Provider/Addressbook.php create mode 100644 apps/dav/lib/CardDAV/Activity/Provider/Base.php create mode 100644 apps/dav/lib/Listener/AddressbookListener.php create mode 100644 core/img/places/contacts-dark.png diff --git a/apps/dav/appinfo/info.xml b/apps/dav/appinfo/info.xml index e16a3ab06c..37a43fca3a 100644 --- a/apps/dav/appinfo/info.xml +++ b/apps/dav/appinfo/info.xml @@ -76,6 +76,7 @@ OCA\DAV\CalDAV\Activity\Provider\Calendar OCA\DAV\CalDAV\Activity\Provider\Event OCA\DAV\CalDAV\Activity\Provider\Todo + OCA\DAV\CardDAV\Activity\Provider\Addressbook diff --git a/apps/dav/composer/composer/autoload_classmap.php b/apps/dav/composer/composer/autoload_classmap.php index 0ab2546b04..1687bc632f 100644 --- a/apps/dav/composer/composer/autoload_classmap.php +++ b/apps/dav/composer/composer/autoload_classmap.php @@ -85,7 +85,10 @@ return array( 'OCA\\DAV\\CalDAV\\WebcalCaching\\Plugin' => $baseDir . '/../lib/CalDAV/WebcalCaching/Plugin.php', 'OCA\\DAV\\CalDAV\\WebcalCaching\\RefreshWebcalService' => $baseDir . '/../lib/CalDAV/WebcalCaching/RefreshWebcalService.php', 'OCA\\DAV\\Capabilities' => $baseDir . '/../lib/Capabilities.php', + 'OCA\\DAV\\CardDAV\\Activity\\Backend' => $baseDir . '/../lib/CardDAV/Activity/Backend.php', 'OCA\\DAV\\CardDAV\\Activity\\Filter' => $baseDir . '/../lib/CardDAV/Activity/Filter.php', + 'OCA\\DAV\\CardDAV\\Activity\\Provider\\Addressbook' => $baseDir . '/../lib/CardDAV/Activity/Provider/Addressbook.php', + 'OCA\\DAV\\CardDAV\\Activity\\Provider\\Base' => $baseDir . '/../lib/CardDAV/Activity/Provider/Base.php', 'OCA\\DAV\\CardDAV\\Activity\\Setting' => $baseDir . '/../lib/CardDAV/Activity/Setting.php', 'OCA\\DAV\\CardDAV\\AddressBook' => $baseDir . '/../lib/CardDAV/AddressBook.php', 'OCA\\DAV\\CardDAV\\AddressBookImpl' => $baseDir . '/../lib/CardDAV/AddressBookImpl.php', @@ -208,6 +211,7 @@ return array( 'OCA\\DAV\\Files\\Sharing\\PublicLinkCheckPlugin' => $baseDir . '/../lib/Files/Sharing/PublicLinkCheckPlugin.php', 'OCA\\DAV\\HookManager' => $baseDir . '/../lib/HookManager.php', 'OCA\\DAV\\Listener\\ActivityUpdaterListener' => $baseDir . '/../lib/Listener/ActivityUpdaterListener.php', + 'OCA\\DAV\\Listener\\AddressbookListener' => $baseDir . '/../lib/Listener/AddressbookListener.php', 'OCA\\DAV\\Listener\\CalendarContactInteractionListener' => $baseDir . '/../lib/Listener/CalendarContactInteractionListener.php', 'OCA\\DAV\\Listener\\CalendarDeletionDefaultUpdaterListener' => $baseDir . '/../lib/Listener/CalendarDeletionDefaultUpdaterListener.php', 'OCA\\DAV\\Listener\\CalendarObjectReminderUpdaterListener' => $baseDir . '/../lib/Listener/CalendarObjectReminderUpdaterListener.php', diff --git a/apps/dav/composer/composer/autoload_static.php b/apps/dav/composer/composer/autoload_static.php index ec4202add5..f2ac36ba90 100644 --- a/apps/dav/composer/composer/autoload_static.php +++ b/apps/dav/composer/composer/autoload_static.php @@ -100,7 +100,10 @@ class ComposerStaticInitDAV 'OCA\\DAV\\CalDAV\\WebcalCaching\\Plugin' => __DIR__ . '/..' . '/../lib/CalDAV/WebcalCaching/Plugin.php', 'OCA\\DAV\\CalDAV\\WebcalCaching\\RefreshWebcalService' => __DIR__ . '/..' . '/../lib/CalDAV/WebcalCaching/RefreshWebcalService.php', 'OCA\\DAV\\Capabilities' => __DIR__ . '/..' . '/../lib/Capabilities.php', + 'OCA\\DAV\\CardDAV\\Activity\\Backend' => __DIR__ . '/..' . '/../lib/CardDAV/Activity/Backend.php', 'OCA\\DAV\\CardDAV\\Activity\\Filter' => __DIR__ . '/..' . '/../lib/CardDAV/Activity/Filter.php', + 'OCA\\DAV\\CardDAV\\Activity\\Provider\\Addressbook' => __DIR__ . '/..' . '/../lib/CardDAV/Activity/Provider/Addressbook.php', + 'OCA\\DAV\\CardDAV\\Activity\\Provider\\Base' => __DIR__ . '/..' . '/../lib/CardDAV/Activity/Provider/Base.php', 'OCA\\DAV\\CardDAV\\Activity\\Setting' => __DIR__ . '/..' . '/../lib/CardDAV/Activity/Setting.php', 'OCA\\DAV\\CardDAV\\AddressBook' => __DIR__ . '/..' . '/../lib/CardDAV/AddressBook.php', 'OCA\\DAV\\CardDAV\\AddressBookImpl' => __DIR__ . '/..' . '/../lib/CardDAV/AddressBookImpl.php', @@ -223,6 +226,7 @@ class ComposerStaticInitDAV 'OCA\\DAV\\Files\\Sharing\\PublicLinkCheckPlugin' => __DIR__ . '/..' . '/../lib/Files/Sharing/PublicLinkCheckPlugin.php', 'OCA\\DAV\\HookManager' => __DIR__ . '/..' . '/../lib/HookManager.php', 'OCA\\DAV\\Listener\\ActivityUpdaterListener' => __DIR__ . '/..' . '/../lib/Listener/ActivityUpdaterListener.php', + 'OCA\\DAV\\Listener\\AddressbookListener' => __DIR__ . '/..' . '/../lib/Listener/AddressbookListener.php', 'OCA\\DAV\\Listener\\CalendarContactInteractionListener' => __DIR__ . '/..' . '/../lib/Listener/CalendarContactInteractionListener.php', 'OCA\\DAV\\Listener\\CalendarDeletionDefaultUpdaterListener' => __DIR__ . '/..' . '/../lib/Listener/CalendarDeletionDefaultUpdaterListener.php', 'OCA\\DAV\\Listener\\CalendarObjectReminderUpdaterListener' => __DIR__ . '/..' . '/../lib/Listener/CalendarObjectReminderUpdaterListener.php', diff --git a/apps/dav/lib/AppInfo/Application.php b/apps/dav/lib/AppInfo/Application.php index 8c1e4f77a1..16615f59b0 100644 --- a/apps/dav/lib/AppInfo/Application.php +++ b/apps/dav/lib/AppInfo/Application.php @@ -51,6 +51,10 @@ use OCA\DAV\CardDAV\CardDavBackend; use OCA\DAV\CardDAV\ContactsManager; use OCA\DAV\CardDAV\PhotoCache; use OCA\DAV\CardDAV\SyncService; +use OCA\DAV\Events\AddressBookCreatedEvent; +use OCA\DAV\Events\AddressBookDeletedEvent; +use OCA\DAV\Events\AddressBookShareUpdatedEvent; +use OCA\DAV\Events\AddressBookUpdatedEvent; use OCA\DAV\Events\CalendarCreatedEvent; use OCA\DAV\Events\CalendarDeletedEvent; use OCA\DAV\Events\CalendarObjectCreatedEvent; @@ -60,6 +64,7 @@ use OCA\DAV\Events\CalendarShareUpdatedEvent; use OCA\DAV\Events\CalendarUpdatedEvent; use OCA\DAV\HookManager; use OCA\DAV\Listener\ActivityUpdaterListener; +use OCA\DAV\Listener\AddressbookListener; use OCA\DAV\Listener\CalendarContactInteractionListener; use OCA\DAV\Listener\CalendarDeletionDefaultUpdaterListener; use OCA\DAV\Listener\CalendarObjectReminderUpdaterListener; @@ -131,6 +136,12 @@ class Application extends App implements IBootstrap { $context->registerEventListener(CalendarObjectDeletedEvent::class, CalendarObjectReminderUpdaterListener::class); $context->registerEventListener(CalendarShareUpdatedEvent::class, CalendarContactInteractionListener::class); + + $context->registerEventListener(AddressBookCreatedEvent::class, AddressbookListener::class); + $context->registerEventListener(AddressBookDeletedEvent::class, AddressbookListener::class); + $context->registerEventListener(AddressBookUpdatedEvent::class, AddressbookListener::class); + $context->registerEventListener(AddressBookShareUpdatedEvent::class, AddressbookListener::class); + $context->registerNotifierService(Notifier::class); } diff --git a/apps/dav/lib/CardDAV/Activity/Backend.php b/apps/dav/lib/CardDAV/Activity/Backend.php new file mode 100644 index 0000000000..a3005f96b2 --- /dev/null +++ b/apps/dav/lib/CardDAV/Activity/Backend.php @@ -0,0 +1,521 @@ + + * + * @author Christoph Wurst + * @author Joas Schilling + * @author Roeland Jago Douma + * @author Thomas Citharel + * + * @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 . + * + */ + +namespace OCA\DAV\CardDAV\Activity; + +use OCA\DAV\CardDAV\Activity\Provider\Addressbook; +use OCP\Activity\IEvent; +use OCP\Activity\IManager as IActivityManager; +use OCP\App\IAppManager; +use OCP\IGroup; +use OCP\IGroupManager; +use OCP\IUser; +use OCP\IUserSession; +use Sabre\CardDAV\Plugin; +use Sabre\VObject\Reader; + +class Backend { + + /** @var IActivityManager */ + protected $activityManager; + + /** @var IGroupManager */ + protected $groupManager; + + /** @var IUserSession */ + protected $userSession; + + /** @var IAppManager */ + protected $appManager; + + public function __construct(IActivityManager $activityManager, + IGroupManager $groupManager, + IUserSession $userSession, + IAppManager $appManager) { + $this->activityManager = $activityManager; + $this->groupManager = $groupManager; + $this->userSession = $userSession; + $this->appManager = $appManager; + } + + /** + * Creates activities when an addressbook was creates + * + * @param array $addressbookData + */ + public function onAddressbookCreate(array $addressbookData): void { + $this->triggerAddressbookActivity(Addressbook::SUBJECT_ADD, $addressbookData); + } + + /** + * Creates activities when a calendar was updated + * + * @param array $calendarData + * @param array $shares + * @param array $properties + */ + public function onAddressbookUpdate(array $calendarData, array $shares, array $properties): void { + $this->triggerAddressbookActivity(Addressbook::SUBJECT_UPDATE, $calendarData, $shares, $properties); + } + + /** + * Creates activities when a calendar was deleted + * + * @param array $calendarData + * @param array $shares + */ + public function onAddressbookDelete(array $calendarData, array $shares): void { + $this->triggerAddressbookActivity(Addressbook::SUBJECT_DELETE, $calendarData, $shares); + } + + /** + * Creates activities for all related users when a calendar was touched + * + * @param string $action + * @param array $addressbookData + * @param array $shares + * @param array $changedProperties + */ + protected function triggerAddressbookActivity($action, array $addressbookData, array $shares = [], array $changedProperties = []) { + if (!isset($addressbookData['principaluri'])) { + return; + } + + $principal = explode('/', $addressbookData['principaluri']); + $owner = array_pop($principal); + + $currentUser = $this->userSession->getUser(); + if ($currentUser instanceof IUser) { + $currentUser = $currentUser->getUID(); + } else { + $currentUser = $owner; + } + + $event = $this->activityManager->generateEvent(); + $event->setApp('dav') + ->setObject('addressbook', (int) $addressbookData['id']) + ->setType('addressbook') + ->setAuthor($currentUser); + + $changedVisibleInformation = array_intersect([ + '{DAV:}displayname', + '{' . Plugin::NS_CARDDAV . '}addressbook-description', + ], array_keys($changedProperties)); + + if (empty($shares) || ($action === Addressbook::SUBJECT_UPDATE && empty($changedVisibleInformation))) { + $users = [$owner]; + } else { + $users = $this->getUsersForShares($shares); + $users[] = $owner; + } + + foreach ($users as $user) { + $event->setAffectedUser($user) + ->setSubject( + $user === $currentUser ? $action . '_self' : $action, + [ + 'actor' => $currentUser, + 'addressbook' => [ + 'id' => (int) $addressbookData['id'], + 'uri' => $addressbookData['uri'], + 'name' => $addressbookData['{DAV:}displayname'], + ], + ] + ); + $this->activityManager->publish($event); + } + } + + /** + * Creates activities for all related users when an addressbook was (un-)shared + * + * @param array $addressbookData + * @param array $shares + * @param array $add + * @param array $remove + */ + public function onAddressbookUpdateShares(array $addressbookData, array $shares, array $add, array $remove) { + $principal = explode('/', $addressbookData['principaluri']); + $owner = $principal[2]; + + $currentUser = $this->userSession->getUser(); + if ($currentUser instanceof IUser) { + $currentUser = $currentUser->getUID(); + } else { + $currentUser = $owner; + } + + $event = $this->activityManager->generateEvent(); + $event->setApp('dav') + ->setObject('addressbook', (int) $addressbookData['id']) + ->setType('addressbook') + ->setAuthor($currentUser); + + foreach ($remove as $principal) { + // principal:principals/users/test + $parts = explode(':', $principal, 2); + if ($parts[0] !== 'principal') { + continue; + } + $principal = explode('/', $parts[1]); + + if ($principal[1] === 'users') { + $this->triggerActivityUser( + $principal[2], + $event, + $addressbookData, + Addressbook::SUBJECT_UNSHARE_USER, + Addressbook::SUBJECT_DELETE . '_self' + ); + + if ($owner !== $principal[2]) { + $parameters = [ + 'actor' => $event->getAuthor(), + 'addressbook' => [ + 'id' => (int) $addressbookData['id'], + 'uri' => $addressbookData['uri'], + 'name' => $addressbookData['{DAV:}displayname'], + ], + 'user' => $principal[2], + ]; + + if ($owner === $event->getAuthor()) { + $subject = Addressbook::SUBJECT_UNSHARE_USER . '_you'; + } elseif ($principal[2] === $event->getAuthor()) { + $subject = Addressbook::SUBJECT_UNSHARE_USER . '_self'; + } else { + $event->setAffectedUser($event->getAuthor()) + ->setSubject(Addressbook::SUBJECT_UNSHARE_USER . '_you', $parameters); + $this->activityManager->publish($event); + + $subject = Addressbook::SUBJECT_UNSHARE_USER . '_by'; + } + + $event->setAffectedUser($owner) + ->setSubject($subject, $parameters); + $this->activityManager->publish($event); + } + } elseif ($principal[1] === 'groups') { + $this->triggerActivityGroup($principal[2], $event, $addressbookData, Addressbook::SUBJECT_UNSHARE_USER); + + $parameters = [ + 'actor' => $event->getAuthor(), + 'addressbook' => [ + 'id' => (int) $addressbookData['id'], + 'uri' => $addressbookData['uri'], + 'name' => $addressbookData['{DAV:}displayname'], + ], + 'group' => $principal[2], + ]; + + if ($owner === $event->getAuthor()) { + $subject = Addressbook::SUBJECT_UNSHARE_GROUP . '_you'; + } else { + $event->setAffectedUser($event->getAuthor()) + ->setSubject(Addressbook::SUBJECT_UNSHARE_GROUP . '_you', $parameters); + $this->activityManager->publish($event); + + $subject = Addressbook::SUBJECT_UNSHARE_GROUP . '_by'; + } + + $event->setAffectedUser($owner) + ->setSubject($subject, $parameters); + $this->activityManager->publish($event); + } + } + + foreach ($add as $share) { + if ($this->isAlreadyShared($share['href'], $shares)) { + continue; + } + + // principal:principals/users/test + $parts = explode(':', $share['href'], 2); + if ($parts[0] !== 'principal') { + continue; + } + $principal = explode('/', $parts[1]); + + if ($principal[1] === 'users') { + $this->triggerActivityUser($principal[2], $event, $addressbookData, Addressbook::SUBJECT_SHARE_USER); + + if ($owner !== $principal[2]) { + $parameters = [ + 'actor' => $event->getAuthor(), + 'addressbook' => [ + 'id' => (int) $addressbookData['id'], + 'uri' => $addressbookData['uri'], + 'name' => $addressbookData['{DAV:}displayname'], + ], + 'user' => $principal[2], + ]; + + if ($owner === $event->getAuthor()) { + $subject = Addressbook::SUBJECT_SHARE_USER . '_you'; + } else { + $event->setAffectedUser($event->getAuthor()) + ->setSubject(Addressbook::SUBJECT_SHARE_USER . '_you', $parameters); + $this->activityManager->publish($event); + + $subject = Addressbook::SUBJECT_SHARE_USER . '_by'; + } + + $event->setAffectedUser($owner) + ->setSubject($subject, $parameters); + $this->activityManager->publish($event); + } + } elseif ($principal[1] === 'groups') { + $this->triggerActivityGroup($principal[2], $event, $addressbookData, Addressbook::SUBJECT_SHARE_USER); + + $parameters = [ + 'actor' => $event->getAuthor(), + 'addressbook' => [ + 'id' => (int) $addressbookData['id'], + 'uri' => $addressbookData['uri'], + 'name' => $addressbookData['{DAV:}displayname'], + ], + 'group' => $principal[2], + ]; + + if ($owner === $event->getAuthor()) { + $subject = Addressbook::SUBJECT_SHARE_GROUP . '_you'; + } else { + $event->setAffectedUser($event->getAuthor()) + ->setSubject(Addressbook::SUBJECT_SHARE_GROUP . '_you', $parameters); + $this->activityManager->publish($event); + + $subject = Addressbook::SUBJECT_SHARE_GROUP . '_by'; + } + + $event->setAffectedUser($owner) + ->setSubject($subject, $parameters); + $this->activityManager->publish($event); + } + } + } + + /** + * Checks if a calendar is already shared with a principal + * + * @param string $principal + * @param array[] $shares + * @return bool + */ + protected function isAlreadyShared(string $principal, array $shares): bool { + foreach ($shares as $share) { + if ($principal === $share['href']) { + return true; + } + } + + return false; + } + + /** + * Creates the given activity for all members of the given group + * + * @param string $gid + * @param IEvent $event + * @param array $properties + * @param string $subject + */ + protected function triggerActivityGroup(string $gid, IEvent $event, array $properties, string $subject): void { + $group = $this->groupManager->get($gid); + + if ($group instanceof IGroup) { + foreach ($group->getUsers() as $user) { + // Exclude current user + if ($user->getUID() !== $event->getAuthor()) { + $this->triggerActivityUser($user->getUID(), $event, $properties, $subject); + } + } + } + } + + /** + * Creates the given activity for the given user + * + * @param string $user + * @param IEvent $event + * @param array $properties + * @param string $subject + * @param string $subjectSelf + */ + protected function triggerActivityUser(string $user, IEvent $event, array $properties, string $subject, string $subjectSelf = ''): void { + $event->setAffectedUser($user) + ->setSubject( + $user === $event->getAuthor() && $subjectSelf ? $subjectSelf : $subject, + [ + 'actor' => $event->getAuthor(), + 'addressbook' => [ + 'id' => (int) $properties['id'], + 'uri' => $properties['uri'], + 'name' => $properties['{DAV:}displayname'], + ], + ] + ); + + $this->activityManager->publish($event); + } + + /** + * Creates activities when a calendar object was created/updated/deleted + * + * @param string $action + * @param array $calendarData + * @param array $shares + * @param array $objectData + */ + public function onTouchCalendarObject($action, array $calendarData, array $shares, array $objectData) { + if (!isset($calendarData['principaluri'])) { + return; + } + + $principal = explode('/', $calendarData['principaluri']); + $owner = array_pop($principal); + + $currentUser = $this->userSession->getUser(); + if ($currentUser instanceof IUser) { + $currentUser = $currentUser->getUID(); + } else { + $currentUser = $owner; + } + + $classification = $objectData['classification'] ?? CalDavBackend::CLASSIFICATION_PUBLIC; + $object = $this->getObjectNameAndType($objectData); + $action = $action . '_' . $object['type']; + + if ($object['type'] === 'todo' && strpos($action, Event::SUBJECT_OBJECT_UPDATE) === 0 && $object['status'] === 'COMPLETED') { + $action .= '_completed'; + } elseif ($object['type'] === 'todo' && strpos($action, Event::SUBJECT_OBJECT_UPDATE) === 0 && $object['status'] === 'NEEDS-ACTION') { + $action .= '_needs_action'; + } + + $event = $this->activityManager->generateEvent(); + $event->setApp('dav') + ->setObject('calendar', (int) $calendarData['id']) + ->setType($object['type'] === 'event' ? 'calendar_event' : 'calendar_todo') + ->setAuthor($currentUser); + + $users = $this->getUsersForShares($shares); + $users[] = $owner; + + // 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 !== $owner) { + // Private events are only shown to the owner + continue; + } + + $params = [ + 'actor' => $event->getAuthor(), + 'calendar' => [ + 'id' => (int) $calendarData['id'], + 'uri' => $calendarData['uri'], + 'name' => $calendarData['{DAV:}displayname'], + ], + 'object' => [ + 'id' => $object['id'], + 'name' => $classification === CalDavBackend::CLASSIFICATION_CONFIDENTIAL && $user !== $owner ? 'Busy' : $object['name'], + 'classified' => $classification === CalDavBackend::CLASSIFICATION_CONFIDENTIAL && $user !== $owner, + ], + ]; + + if ($object['type'] === 'event' && strpos($action, Event::SUBJECT_OBJECT_DELETE) === false && $this->appManager->isEnabledForUser('calendar')) { + $params['object']['link']['object_uri'] = $objectData['uri']; + $params['object']['link']['calendar_uri'] = $calendarData['uri']; + $params['object']['link']['owner'] = $owner; + } + + + $event->setAffectedUser($user) + ->setSubject( + $user === $currentUser ? $action . '_self' : $action, + $params + ); + + $this->activityManager->publish($event); + } + } + + /** + * @param array $objectData + * @return string[]|bool + */ + protected function getObjectNameAndType(array $objectData) { + $vObject = Reader::read($objectData['calendardata']); + $component = $componentType = null; + foreach ($vObject->getComponents() as $component) { + if (in_array($component->name, ['VEVENT', 'VTODO'])) { + $componentType = $component->name; + break; + } + } + + if (!$componentType) { + // Calendar objects must have a VEVENT or VTODO component + return false; + } + + if ($componentType === 'VEVENT') { + return ['id' => (string) $component->UID, 'name' => (string) $component->SUMMARY, 'type' => 'event']; + } + return ['id' => (string) $component->UID, 'name' => (string) $component->SUMMARY, 'type' => 'todo', 'status' => (string) $component->STATUS]; + } + + /** + * Get all users that have access to a given calendar + * + * @param array $shares + * @return string[] + */ + protected function getUsersForShares(array $shares) { + $users = $groups = []; + foreach ($shares as $share) { + $principal = explode('/', $share['{http://owncloud.org/ns}principal']); + if ($principal[1] === 'users') { + $users[] = $principal[2]; + } elseif ($principal[1] === 'groups') { + $groups[] = $principal[2]; + } + } + + if (!empty($groups)) { + foreach ($groups as $gid) { + $group = $this->groupManager->get($gid); + if ($group instanceof IGroup) { + foreach ($group->getUsers() as $user) { + $users[] = $user->getUID(); + } + } + } + } + + return array_unique($users); + } +} diff --git a/apps/dav/lib/CardDAV/Activity/Provider/Addressbook.php b/apps/dav/lib/CardDAV/Activity/Provider/Addressbook.php new file mode 100644 index 0000000000..6bd550213e --- /dev/null +++ b/apps/dav/lib/CardDAV/Activity/Provider/Addressbook.php @@ -0,0 +1,203 @@ + + * + * @author Joas Schilling + * + * @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 . + * + */ + +namespace OCA\DAV\CardDAV\Activity\Provider; + +use OCP\Activity\IEvent; +use OCP\Activity\IEventMerger; +use OCP\Activity\IManager; +use OCP\IGroupManager; +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\IUserManager; +use OCP\L10N\IFactory; + +class Addressbook extends Base { + public const SUBJECT_ADD = 'addressbook_add'; + public const SUBJECT_UPDATE = 'addressbook_update'; + public const SUBJECT_DELETE = 'addressbook_delete'; + public const SUBJECT_SHARE_USER = 'addressbook_user_share'; + public const SUBJECT_SHARE_GROUP = 'addressbook_group_share'; + public const SUBJECT_UNSHARE_USER = 'addressbook_user_unshare'; + public const SUBJECT_UNSHARE_GROUP = 'addressbook_group_unshare'; + + /** @var IFactory */ + protected $languageFactory; + + /** @var IL10N */ + protected $l; + + /** @var IManager */ + protected $activityManager; + + /** @var IEventMerger */ + protected $eventMerger; + + /** + * @param IFactory $languageFactory + * @param IURLGenerator $url + * @param IManager $activityManager + * @param IUserManager $userManager + * @param IGroupManager $groupManager + * @param IEventMerger $eventMerger + */ + public function __construct(IFactory $languageFactory, + IURLGenerator $url, + IManager $activityManager, + IUserManager $userManager, + IGroupManager $groupManager, + IEventMerger $eventMerger) { + parent::__construct($userManager, $groupManager, $url); + $this->languageFactory = $languageFactory; + $this->activityManager = $activityManager; + $this->eventMerger = $eventMerger; + } + + /** + * @param string $language + * @param IEvent $event + * @param IEvent|null $previousEvent + * @return IEvent + * @throws \InvalidArgumentException + * @since 11.0.0 + */ + public function parse($language, IEvent $event, IEvent $previousEvent = null): IEvent { + if ($event->getApp() !== 'dav' || $event->getType() !== 'addressbook') { + throw new \InvalidArgumentException(); + } + + $this->l = $this->languageFactory->get('dav', $language); + + if ($this->activityManager->getRequirePNG()) { + $event->setIcon($this->url->getAbsoluteURL($this->url->imagePath('core', 'places/contacts-dark.png'))); + } else { + $event->setIcon($this->url->getAbsoluteURL($this->url->imagePath('core', 'places/contacts.svg'))); + } + + if ($event->getSubject() === self::SUBJECT_ADD) { + $subject = $this->l->t('{actor} created addressbook {addressbook}'); + } elseif ($event->getSubject() === self::SUBJECT_ADD . '_self') { + $subject = $this->l->t('You created addressbook {addressbook}'); + } elseif ($event->getSubject() === self::SUBJECT_DELETE) { + $subject = $this->l->t('{actor} deleted addressbook {addressbook}'); + } elseif ($event->getSubject() === self::SUBJECT_DELETE . '_self') { + $subject = $this->l->t('You deleted addressbook {addressbook}'); + } elseif ($event->getSubject() === self::SUBJECT_UPDATE) { + $subject = $this->l->t('{actor} updated addressbook {addressbook}'); + } elseif ($event->getSubject() === self::SUBJECT_UPDATE . '_self') { + $subject = $this->l->t('You updated addressbook {addressbook}'); + } elseif ($event->getSubject() === self::SUBJECT_SHARE_USER) { + $subject = $this->l->t('{actor} shared addressbook {addressbook} with you'); + } elseif ($event->getSubject() === self::SUBJECT_SHARE_USER . '_you') { + $subject = $this->l->t('You shared addressbook {addressbook} with {user}'); + } elseif ($event->getSubject() === self::SUBJECT_SHARE_USER . '_by') { + $subject = $this->l->t('{actor} shared addressbook {addressbook} with {user}'); + } elseif ($event->getSubject() === self::SUBJECT_UNSHARE_USER) { + $subject = $this->l->t('{actor} unshared addressbook {addressbook} from you'); + } elseif ($event->getSubject() === self::SUBJECT_UNSHARE_USER . '_you') { + $subject = $this->l->t('You unshared addressbook {addressbook} from {user}'); + } elseif ($event->getSubject() === self::SUBJECT_UNSHARE_USER . '_by') { + $subject = $this->l->t('{actor} unshared addressbook {addressbook} from {user}'); + } elseif ($event->getSubject() === self::SUBJECT_UNSHARE_USER . '_self') { + $subject = $this->l->t('{actor} unshared addressbook {addressbook} from themselves'); + } elseif ($event->getSubject() === self::SUBJECT_SHARE_GROUP . '_you') { + $subject = $this->l->t('You shared addressbook {addressbook} with group {group}'); + } elseif ($event->getSubject() === self::SUBJECT_SHARE_GROUP . '_by') { + $subject = $this->l->t('{actor} shared addressbook {addressbook} with group {group}'); + } elseif ($event->getSubject() === self::SUBJECT_UNSHARE_GROUP . '_you') { + $subject = $this->l->t('You unshared addressbook {addressbook} from group {group}'); + } elseif ($event->getSubject() === self::SUBJECT_UNSHARE_GROUP . '_by') { + $subject = $this->l->t('{actor} unshared addressbook {addressbook} from group {group}'); + } else { + throw new \InvalidArgumentException(); + } + + $parsedParameters = $this->getParameters($event); + $this->setSubjects($event, $subject, $parsedParameters); + + $event = $this->eventMerger->mergeEvents('addressbook', $event, $previousEvent); + + if ($event->getChildEvent() === null) { + if (isset($parsedParameters['user'])) { + // Couldn't group by calendar, maybe we can group by users + $event = $this->eventMerger->mergeEvents('user', $event, $previousEvent); + } elseif (isset($parsedParameters['group'])) { + // Couldn't group by calendar, maybe we can group by groups + $event = $this->eventMerger->mergeEvents('group', $event, $previousEvent); + } + } + + return $event; + } + + protected function getParameters(IEvent $event): array { + $subject = $event->getSubject(); + $parameters = $event->getSubjectParameters(); + + switch ($subject) { + case self::SUBJECT_ADD: + case self::SUBJECT_ADD . '_self': + case self::SUBJECT_DELETE: + case self::SUBJECT_DELETE . '_self': + case self::SUBJECT_UPDATE: + case self::SUBJECT_UPDATE . '_self': + case self::SUBJECT_SHARE_USER: + case self::SUBJECT_UNSHARE_USER: + case self::SUBJECT_UNSHARE_USER . '_self': + return [ + 'actor' => $this->generateUserParameter($parameters['actor']), + 'addressbook' => $this->generateAddressbookParameter($parameters['addressbook'], $this->l), + ]; + case self::SUBJECT_SHARE_USER . '_you': + case self::SUBJECT_UNSHARE_USER . '_you': + return [ + 'addressbook' => $this->generateAddressbookParameter($parameters['addressbook'], $this->l), + 'user' => $this->generateUserParameter($parameters['user']), + ]; + case self::SUBJECT_SHARE_USER . '_by': + case self::SUBJECT_UNSHARE_USER . '_by': + return [ + 'actor' => $this->generateUserParameter($parameters['actor']), + 'addressbook' => $this->generateAddressbookParameter($parameters['addressbook'], $this->l), + 'user' => $this->generateUserParameter($parameters['user']), + ]; + case self::SUBJECT_SHARE_GROUP . '_you': + case self::SUBJECT_UNSHARE_GROUP . '_you': + return [ + 'addressbook' => $this->generateAddressbookParameter($parameters['addressbook'], $this->l), + 'group' => $this->generateGroupParameter($parameters['group']), + ]; + case self::SUBJECT_SHARE_GROUP . '_by': + case self::SUBJECT_UNSHARE_GROUP . '_by': + return [ + 'actor' => $this->generateUserParameter($parameters['actor']), + 'addressbook' => $this->generateAddressbookParameter($parameters['addressbook'], $this->l), + 'group' => $this->generateGroupParameter($parameters['group']), + ]; + } + + throw new \InvalidArgumentException(); + } +} diff --git a/apps/dav/lib/CardDAV/Activity/Provider/Base.php b/apps/dav/lib/CardDAV/Activity/Provider/Base.php new file mode 100644 index 0000000000..1032ad22a0 --- /dev/null +++ b/apps/dav/lib/CardDAV/Activity/Provider/Base.php @@ -0,0 +1,159 @@ + + * + * @author Joas Schilling + * + * @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 . + * + */ + +namespace OCA\DAV\CardDAV\Activity\Provider; + +use OCA\DAV\CardDAV\CardDavBackend; +use OCP\Activity\IEvent; +use OCP\Activity\IProvider; +use OCP\IGroup; +use OCP\IGroupManager; +use OCP\IL10N; +use OCP\IURLGenerator; +use OCP\IUser; +use OCP\IUserManager; + +abstract class Base implements IProvider { + + /** @var IUserManager */ + protected $userManager; + + /** @var string[] */ + protected $userDisplayNames = []; + + /** @var IGroupManager */ + protected $groupManager; + + /** @var string[] */ + protected $groupDisplayNames = []; + + /** @var IURLGenerator */ + protected $url; + + /** + * @param IUserManager $userManager + * @param IGroupManager $groupManager + * @param IURLGenerator $urlGenerator + */ + public function __construct(IUserManager $userManager, IGroupManager $groupManager, IURLGenerator $urlGenerator) { + $this->userManager = $userManager; + $this->groupManager = $groupManager; + $this->url = $urlGenerator; + } + + /** + * @param IEvent $event + * @param string $subject + * @param array $parameters + */ + protected function setSubjects(IEvent $event, string $subject, array $parameters): void { + $placeholders = $replacements = []; + foreach ($parameters as $placeholder => $parameter) { + $placeholders[] = '{' . $placeholder . '}'; + $replacements[] = $parameter['name']; + } + + $event->setParsedSubject(str_replace($placeholders, $replacements, $subject)) + ->setRichSubject($subject, $parameters); + } + + /** + * @param array $data + * @param IL10N $l + * @return array + */ + protected function generateAddressbookParameter(array $data, IL10N $l): array { + if ($data['uri'] === CardDavBackend::PERSONAL_ADDRESSBOOK_URI && + $data['name'] === CardDavBackend::PERSONAL_ADDRESSBOOK_NAME) { + return [ + 'type' => 'addressbook', + 'id' => $data['id'], + 'name' => $l->t('Personal'), + ]; + } + + return [ + 'type' => 'addressbook', + 'id' => $data['id'], + 'name' => $data['name'], + ]; + } + + /** + * @param string $uid + * @return array + */ + protected function generateUserParameter(string $uid): array { + if (!isset($this->userDisplayNames[$uid])) { + $this->userDisplayNames[$uid] = $this->getUserDisplayName($uid); + } + + return [ + 'type' => 'user', + 'id' => $uid, + 'name' => $this->userDisplayNames[$uid], + ]; + } + + /** + * @param string $uid + * @return string + */ + protected function getUserDisplayName(string $uid): string { + $user = $this->userManager->get($uid); + if ($user instanceof IUser) { + return $user->getDisplayName(); + } + return $uid; + } + + /** + * @param string $gid + * @return array + */ + protected function generateGroupParameter(string $gid): array { + if (!isset($this->groupDisplayNames[$gid])) { + $this->groupDisplayNames[$gid] = $this->getGroupDisplayName($gid); + } + + return [ + 'type' => 'user-group', + 'id' => $gid, + 'name' => $this->groupDisplayNames[$gid], + ]; + } + + /** + * @param string $gid + * @return string + */ + protected function getGroupDisplayName(string $gid): string { + $group = $this->groupManager->get($gid); + if ($group instanceof IGroup) { + return $group->getDisplayName(); + } + return $gid; + } +} diff --git a/apps/dav/lib/Listener/AddressbookListener.php b/apps/dav/lib/Listener/AddressbookListener.php new file mode 100644 index 0000000000..35b5cf1541 --- /dev/null +++ b/apps/dav/lib/Listener/AddressbookListener.php @@ -0,0 +1,122 @@ + + * + * @author Joas Schilling + * + * @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 . + */ + +namespace OCA\DAV\Listener; + +use OCA\DAV\CardDAV\Activity\Backend as ActivityBackend; +use OCA\DAV\Events\AddressBookCreatedEvent; +use OCA\DAV\Events\AddressBookDeletedEvent; +use OCA\DAV\Events\AddressBookShareUpdatedEvent; +use OCA\DAV\Events\AddressBookUpdatedEvent; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use Psr\Log\LoggerInterface; +use Throwable; +use function sprintf; + +class AddressbookListener implements IEventListener { + + /** @var ActivityBackend */ + private $activityBackend; + + /** @var LoggerInterface */ + private $logger; + + public function __construct(ActivityBackend $activityBackend, + LoggerInterface $logger) { + $this->activityBackend = $activityBackend; + $this->logger = $logger; + } + + public function handle(Event $event): void { + if ($event instanceof AddressBookCreatedEvent) { + try { + $this->activityBackend->onAddressbookCreate( + $event->getAddressBookData() + ); + + $this->logger->debug( + sprintf('Activity generated for new addressbook %d', $event->getAddressBookId()) + ); + } catch (Throwable $e) { + // Any error with activities shouldn't abort the addressbook creation, so we just log it + $this->logger->error('Error generating activities for a new addressbook: ' . $e->getMessage(), [ + 'exception' => $e, + ]); + } + } else if ($event instanceof AddressBookUpdatedEvent) { + try { + $this->activityBackend->onAddressbookUpdate( + $event->getAddressBookData(), + $event->getShares(), + $event->getMutations() + ); + + $this->logger->debug( + sprintf('Activity generated for changed addressbook %d', $event->getAddressBookId()) + ); + } catch (Throwable $e) { + // Any error with activities shouldn't abort the addressbook update, so we just log it + $this->logger->error('Error generating activities for a changed addressbook: ' . $e->getMessage(), [ + 'exception' => $e, + ]); + } + } else if ($event instanceof AddressBookDeletedEvent) { + try { + $this->activityBackend->onAddressbookDelete( + $event->getAddressBookData(), + $event->getShares() + ); + + $this->logger->debug( + sprintf('Activity generated for deleted addressbook %d', $event->getAddressBookId()) + ); + } catch (Throwable $e) { + // Any error with activities shouldn't abort the addressbook deletion, so we just log it + $this->logger->error('Error generating activities for a deleted addressbook: ' . $e->getMessage(), [ + 'exception' => $e, + ]); + } + } else if ($event instanceof AddressBookShareUpdatedEvent) { + try { + $this->activityBackend->onAddressbookUpdateShares( + $event->getAddressBookData(), + $event->getOldShares(), + $event->getAdded(), + $event->getRemoved() + ); + + $this->logger->debug( + sprintf('Activity generated for (un)sharing addressbook %d', $event->getAddressBookId()) + ); + } catch (Throwable $e) { + // Any error with activities shouldn't abort the addressbook creation, so we just log it + $this->logger->error('Error generating activities for (un)sharing addressbook: ' . $e->getMessage(), [ + 'exception' => $e, + ]); + } + } + } +} diff --git a/core/img/places/contacts-dark.png b/core/img/places/contacts-dark.png new file mode 100644 index 0000000000000000000000000000000000000000..438761b1a5bebc310de35be0a97cad3bf20f4744 GIT binary patch literal 595 zcmV-Z0<8UsP)15bU)S!53Ec zHe#dx1uBAI5fLpTBGC|R3iMm+Fwi#hk4GyBcX%;i5o zH%{R(=I{j*xQKPFHs6MqSW2lG>}!>IAEqK4L@454YPiM*Kx0xV&mXn@fiq2b8OT6{^h zI>{{QXIYyQurEH}7MV@NEWQ>E@GeK}Lwt@m5a3zbKvIB-9I^4bXAz7wz;#%9A8W&H z5O%#H{om6_0JxeUvVh%nykq#4V}7|&bLhlmozU?V?&%Ekk*L)aZr6!zP2qJ&-V?r~ zw;AI>ob=~DKO$_sNelNuLVy>I8t=zl{74Y1tyegMp7>rD?kDixV0{teemu-EO{!10 zj*l6fZ@7U2LaP#Swh8lQYEo!zcYT0sZ36CqMTrS=hA>Glsg*aB^qS(qcGEgMI z&`JkLy-xiFJSjrzz(L%?XUp&VeAGIGQtAPbf>qdwy~5|#=2-o4tS1Jp2-mfWaC*KL h&Vo_l-?r#q!EXjvS>aLS#>)Tz002ovPDHLkV1f^53g!R+ literal 0 HcmV?d00001