- Introduce a new CalendarObjectMovedEvent typed event dedicated for this operation - Handle the event in the activity backend and add new appropriate activity subjects Signed-off-by: Thomas Citharel <tcit@tcit.fr>tags/v25.0.0beta1
@@ -209,6 +209,7 @@ return array( | |||
'OCA\\DAV\\Events\\CalendarMovedToTrashEvent' => $baseDir . '/../lib/Events/CalendarMovedToTrashEvent.php', | |||
'OCA\\DAV\\Events\\CalendarObjectCreatedEvent' => $baseDir . '/../lib/Events/CalendarObjectCreatedEvent.php', | |||
'OCA\\DAV\\Events\\CalendarObjectDeletedEvent' => $baseDir . '/../lib/Events/CalendarObjectDeletedEvent.php', | |||
'OCA\\DAV\\Events\\CalendarObjectMovedEvent' => $baseDir . '/../lib/Events/CalendarObjectMovedEvent.php', | |||
'OCA\\DAV\\Events\\CalendarObjectMovedToTrashEvent' => $baseDir . '/../lib/Events/CalendarObjectMovedToTrashEvent.php', | |||
'OCA\\DAV\\Events\\CalendarObjectRestoredEvent' => $baseDir . '/../lib/Events/CalendarObjectRestoredEvent.php', | |||
'OCA\\DAV\\Events\\CalendarObjectUpdatedEvent' => $baseDir . '/../lib/Events/CalendarObjectUpdatedEvent.php', |
@@ -224,6 +224,7 @@ class ComposerStaticInitDAV | |||
'OCA\\DAV\\Events\\CalendarMovedToTrashEvent' => __DIR__ . '/..' . '/../lib/Events/CalendarMovedToTrashEvent.php', | |||
'OCA\\DAV\\Events\\CalendarObjectCreatedEvent' => __DIR__ . '/..' . '/../lib/Events/CalendarObjectCreatedEvent.php', | |||
'OCA\\DAV\\Events\\CalendarObjectDeletedEvent' => __DIR__ . '/..' . '/../lib/Events/CalendarObjectDeletedEvent.php', | |||
'OCA\\DAV\\Events\\CalendarObjectMovedEvent' => __DIR__ . '/..' . '/../lib/Events/CalendarObjectMovedEvent.php', | |||
'OCA\\DAV\\Events\\CalendarObjectMovedToTrashEvent' => __DIR__ . '/..' . '/../lib/Events/CalendarObjectMovedToTrashEvent.php', | |||
'OCA\\DAV\\Events\\CalendarObjectRestoredEvent' => __DIR__ . '/..' . '/../lib/Events/CalendarObjectRestoredEvent.php', | |||
'OCA\\DAV\\Events\\CalendarObjectUpdatedEvent' => __DIR__ . '/..' . '/../lib/Events/CalendarObjectUpdatedEvent.php', |
@@ -57,6 +57,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; | |||
@@ -154,6 +155,8 @@ 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); |
@@ -438,6 +438,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') { | |||
@@ -493,9 +498,104 @@ 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']); |
@@ -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. |
@@ -69,6 +69,10 @@ class Todo extends Event { | |||
$subject = $this->l->t('{actor} reopened todo {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}'); | |||
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_MOVE . '_todo') { | |||
$subject = $this->l->t('{actor} moved todo {todo} from list {sourceCalendar} to list {targetCalendar}'); | |||
} elseif ($event->getSubject() === self::SUBJECT_OBJECT_MOVE . '_todo_self') { | |||
$subject = $this->l->t('You moved todo {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. |
@@ -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; | |||
@@ -1341,13 +1342,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; | |||
} | |||
@@ -1365,21 +1367,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; | |||
} |
@@ -450,7 +450,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; |
@@ -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(); | |||
} | |||
} |
@@ -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; | |||
} | |||
} |
@@ -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 |