diff --git a/src/Bundle/ChillCalendarBundle/Entity/Calendar.php b/src/Bundle/ChillCalendarBundle/Entity/Calendar.php index c0d1fbb9a..25486459f 100644 --- a/src/Bundle/ChillCalendarBundle/Entity/Calendar.php +++ b/src/Bundle/ChillCalendarBundle/Entity/Calendar.php @@ -38,7 +38,10 @@ use Symfony\Component\Validator\Mapping\ClassMetadata; use function in_array; /** - * @ORM\Table(name="chill_calendar.calendar", indexes={@ORM\Index(name="idx_calendar_remote", columns={"remoteId"})})) + * @ORM\Table( + * name="chill_calendar.calendar", + * uniqueConstraints={@ORM\UniqueConstraint(name="idx_calendar_remote", columns={"remoteId"}, options={"where": "remoteId <> ''"})} + * ) * @ORM\Entity */ class Calendar implements TrackCreationInterface, TrackUpdateInterface diff --git a/src/Bundle/ChillCalendarBundle/Entity/CalendarRange.php b/src/Bundle/ChillCalendarBundle/Entity/CalendarRange.php index 5bda987fc..76596347c 100644 --- a/src/Bundle/ChillCalendarBundle/Entity/CalendarRange.php +++ b/src/Bundle/ChillCalendarBundle/Entity/CalendarRange.php @@ -21,7 +21,10 @@ use Doctrine\ORM\Mapping as ORM; use Symfony\Component\Serializer\Annotation\Groups; /** - * @ORM\Table(name="chill_calendar.calendar_range", indexes={@ORM\Index(name="idx_calendar_range_remote", columns={"remoteId"})}) + * @ORM\Table( + * name="chill_calendar.calendar_range", + * uniqueConstraints={@ORM\UniqueConstraint(name="idx_calendar_range_remote", columns={"remoteId"}, options={"where": "remoteId <> ''"})} + * ) * @ORM\Entity */ class CalendarRange implements TrackCreationInterface, TrackUpdateInterface diff --git a/src/Bundle/ChillCalendarBundle/Entity/Invite.php b/src/Bundle/ChillCalendarBundle/Entity/Invite.php index e7f25d932..e53ca1ca1 100644 --- a/src/Bundle/ChillCalendarBundle/Entity/Invite.php +++ b/src/Bundle/ChillCalendarBundle/Entity/Invite.php @@ -26,9 +26,10 @@ use Symfony\Component\Serializer\Annotation as Serializer; * The event/calendar in the user may have a different id than the mainUser. We add then fields to store the * remote id of this event in the remote calendar. * - * @ORM\Table(name="chill_calendar.invite", indexes={ - * @ORM\Index(name="idx_calendar_invite_remote", columns={"remoteId"}) - * }) + * @ORM\Table( + * name="chill_calendar.invite", + * uniqueConstraints={@ORM\UniqueConstraint(name="idx_calendar_invite_remote", columns={"remoteId"}, options={"where": "remoteId <> ''"})} + * ) * @ORM\Entity */ class Invite implements TrackUpdateInterface, TrackCreationInterface diff --git a/src/Bundle/ChillCalendarBundle/Messenger/Handler/MSGraphChangeNotificationHandler.php b/src/Bundle/ChillCalendarBundle/Messenger/Handler/MSGraphChangeNotificationHandler.php index 410c5f9a2..4e201af8e 100644 --- a/src/Bundle/ChillCalendarBundle/Messenger/Handler/MSGraphChangeNotificationHandler.php +++ b/src/Bundle/ChillCalendarBundle/Messenger/Handler/MSGraphChangeNotificationHandler.php @@ -23,7 +23,7 @@ use Psr\Log\LoggerInterface; use Symfony\Component\Messenger\Handler\MessageHandlerInterface; /** - * Handle notification of chagnes from MSGraph + * Handle notification of chagnes from MSGraph. * * @AsMessageHandler */ @@ -97,7 +97,7 @@ class MSGraphChangeNotificationHandler implements MessageHandlerInterface $this->remoteToLocalSyncer->handleInviteSync($invite, $notification, $user); $this->em->flush(); } else { - $this->logger->info(__CLASS__." id not found in any calendar, calendar range nor invite"); + $this->logger->info(__CLASS__ . ' id not found in any calendar, calendar range nor invite'); } } } diff --git a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/RemoteEventConverter.php b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/RemoteEventConverter.php index e87952308..4ff42cd6f 100644 --- a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/RemoteEventConverter.php +++ b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/RemoteEventConverter.php @@ -31,7 +31,7 @@ class RemoteEventConverter { /** * valid when the remote string contains also a timezone, like in - * lastModifiedDate + * lastModifiedDate. */ public const REMOTE_DATETIMEZONE_FORMAT = 'Y-m-d\\TH:i:s.u?P'; diff --git a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/RemoteToLocalSync/CalendarRangeSyncer.php b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/RemoteToLocalSync/CalendarRangeSyncer.php index d92e263fa..298bbc7b5 100644 --- a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/RemoteToLocalSync/CalendarRangeSyncer.php +++ b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/RemoteToLocalSync/CalendarRangeSyncer.php @@ -17,6 +17,7 @@ use Chill\CalendarBundle\RemoteCalendar\Connector\MSGraph\RemoteEventConverter; use Chill\MainBundle\Entity\User; use Doctrine\ORM\EntityManagerInterface; use Psr\Log\LoggerInterface; +use RuntimeException; use Symfony\Contracts\HttpClient\Exception\ClientExceptionInterface; use Symfony\Contracts\HttpClient\HttpClientInterface; @@ -29,8 +30,6 @@ class CalendarRangeSyncer private HttpClientInterface $machineHttpClient; /** - * @param EntityManagerInterface $em - * @param LoggerInterface $logger * @param MachineHttpClient $machineHttpClient */ public function __construct( @@ -43,7 +42,6 @@ class CalendarRangeSyncer $this->machineHttpClient = $machineHttpClient; } - public function handleCalendarRangeSync(CalendarRange $calendarRange, array $notification, User $user): void { switch ($notification['changeType']) { @@ -94,11 +92,12 @@ class CalendarRangeSyncer 'lastModifiedDateTime' => $lastModified->getTimestamp(), 'changeKey' => $new['changeKey'], ]) - ->preventEnqueueChanges = true - ; + ->preventEnqueueChanges = true; + break; + default: - throw new \RuntimeException('This changeType is not suppored: '.$notification['changeType']); + throw new RuntimeException('This changeType is not suppored: ' . $notification['changeType']); } } } diff --git a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/RemoteToLocalSync/CalendarSyncer.php b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/RemoteToLocalSync/CalendarSyncer.php index ace9515ea..d04dc7005 100644 --- a/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/RemoteToLocalSync/CalendarSyncer.php +++ b/src/Bundle/ChillCalendarBundle/RemoteCalendar/Connector/MSGraph/RemoteToLocalSync/CalendarSyncer.php @@ -1,13 +1,27 @@ logger = $logger; $this->machineHttpClient = $machineHttpClient; + $this->userRepository = $userRepository; } public function handleCalendarSync(Calendar $calendar, array $notification, User $user): void @@ -30,14 +43,16 @@ class CalendarSyncer switch ($notification['changeType']) { case 'deleted': $this->handleDeleteCalendar($calendar, $notification, $user); + break; case 'updated': $this->handleUpdateCalendar($calendar, $notification, $user); + break; default: - throw new \RuntimeException("this change type is not supported: ".$notification['changeType']); + throw new RuntimeException('this change type is not supported: ' . $notification['changeType']); } } @@ -45,8 +60,7 @@ class CalendarSyncer { $calendar ->setStatus(Calendar::STATUS_CANCELED) - ->setCalendarRange(null) - ; + ->setCalendarRange(null); $calendar->preventEnqueueChanges = true; } @@ -64,6 +78,10 @@ class CalendarSyncer ]); } + if (false === $new['isOrganizer']) { + return; + } + $lastModified = RemoteEventConverter::convertStringDateWithTimezone( $new['lastModifiedDateTime'] ); @@ -77,17 +95,91 @@ class CalendarSyncer return; } + $this->syncAttendees($calendar, $new['attendees']); + $startDate = RemoteEventConverter::convertStringDateWithoutTimezone($new['start']['dateTime']); $endDate = RemoteEventConverter::convertStringDateWithoutTimezone($new['end']['dateTime']); + if ($startDate->getTimestamp() !== $calendar->getStartDate()->getTimestamp()) { + $calendar->setStartDate($startDate)->setStatus(Calendar::STATUS_MOVED); + } + + if ($endDate->getTimestamp() !== $calendar->getEndDate()->getTimestamp()) { + $calendar->setEndDate($endDate)->setStatus(Calendar::STATUS_MOVED); + } + $calendar - ->setStartDate($startDate)->setEndDate($endDate) - ->setStatus(Calendar::STATUS_MOVED) ->addRemoteAttributes([ 'lastModifiedDateTime' => $lastModified->getTimestamp(), 'changeKey' => $new['changeKey'], ]) - ->preventEnqueueChanges = true - ; + ->preventEnqueueChanges = true; + } + + private function syncAttendees(Calendar $calendar, array $attendees): void + { + $emails = []; + + foreach ($attendees as $attendee) { + $status = $attendee['status']['response']; + + if ('organizer' === $status) { + continue; + } + + $email = $attendee['emailAddress']['address']; + $emails[] = strtolower($email); + $user = $this->userRepository->findOneByUsernameOrEmail($email); + + if (null === $user) { + continue; + } + + if (!$calendar->isInvited($user)) { + $calendar->addUser($user); + } + + $invite = $calendar->getInviteForUser($user); + + switch ($status) { + // none, organizer, tentativelyAccepted, accepted, declined, notResponded. + case 'none': + case 'notResponded': + $invite->setStatus(Invite::PENDING); + + break; + + case 'organizer': + throw new LogicException('should not happens'); + + break; + + case 'tentativelyAccepted': + $invite->setStatus(Invite::TENTATIVELY_ACCEPTED); + + break; + + case 'accepted': + $invite->setStatus(Invite::ACCEPTED); + + break; + + case 'declined': + $invite->setStatus(Invite::DECLINED); + + break; + + default: + throw new LogicException('should not happens, not implemented: ' . $status); + + break; + } + } + + foreach ($calendar->getUsers() as $user) { + if (!in_array(strtolower($user->getEmailCanonical()), $emails, true)) { + $calendar->removeUser($user); + } + } } } diff --git a/src/Bundle/ChillCalendarBundle/RemoteCalendar/DependencyInjection/RemoteCalendarCompilerPass.php b/src/Bundle/ChillCalendarBundle/RemoteCalendar/DependencyInjection/RemoteCalendarCompilerPass.php index df10751ac..f6631f0cc 100644 --- a/src/Bundle/ChillCalendarBundle/RemoteCalendar/DependencyInjection/RemoteCalendarCompilerPass.php +++ b/src/Bundle/ChillCalendarBundle/RemoteCalendar/DependencyInjection/RemoteCalendarCompilerPass.php @@ -39,7 +39,7 @@ class RemoteCalendarCompilerPass implements CompilerPassInterface if ($config['remote_calendars_sync']['microsoft_graph']['enabled']) { $connector = MSGraphRemoteCalendarConnector::class; - $container->setAlias(HttpClientInterface::class.' $machineHttpClient', MachineHttpClient::class); + $container->setAlias(HttpClientInterface::class . ' $machineHttpClient', MachineHttpClient::class); } else { // remove services which cannot be loaded $container->removeDefinition(MapAndSubscribeUserCalendarCommand::class); diff --git a/src/Bundle/ChillCalendarBundle/Tests/Controller/RemoteCalendarMSGraphSyncControllerTest.php b/src/Bundle/ChillCalendarBundle/Tests/Controller/RemoteCalendarMSGraphSyncControllerTest.php index 39b414426..2be0cef24 100644 --- a/src/Bundle/ChillCalendarBundle/Tests/Controller/RemoteCalendarMSGraphSyncControllerTest.php +++ b/src/Bundle/ChillCalendarBundle/Tests/Controller/RemoteCalendarMSGraphSyncControllerTest.php @@ -61,7 +61,7 @@ final class RemoteCalendarMSGraphSyncControllerTest extends WebTestCase $this->assertResponseIsSuccessful(); $this->assertResponseStatusCodeSame(202); - /* @var InMemoryTransport $transport */ + /** @var InMemoryTransport $transport */ $transport = self::$container->get('messenger.transport.async'); $this->assertCount(1, $transport->getSent()); } diff --git a/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/CalendarRangeSyncerTest.php b/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/CalendarRangeSyncerTest.php index af2009fad..6ff112956 100644 --- a/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/CalendarRangeSyncerTest.php +++ b/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/CalendarRangeSyncerTest.php @@ -1,11 +1,21 @@ prophesize(EntityManagerInterface::class); - $machineHttpClient = new MockHttpClient([ - new MockResponse(self::REMOTE_CALENDAR_RANGE, ['http_code' => 200]) - ]); - - $calendarRangeSyncer = new CalendarRangeSyncer( - $em->reveal(), - new NullLogger(), - $machineHttpClient - ); - - $calendarRange = new CalendarRange(); - $calendarRange - ->setUser($user = new User()) - ->setStartDate(new \DateTimeImmutable('2020-01-01 15:00:00')) - ->setEndDate(new \DateTimeImmutable('2020-01-01 15:30:00')) - ->addRemoteAttributes([ - 'lastModifiedDateTime' => 0, - 'changeKey' => 'abc' - ]); - $notification = json_decode(self::NOTIF_UPDATE, true); - - $calendarRangeSyncer->handleCalendarRangeSync( - $calendarRange, - $notification['value'][0], - $user); - - $this->assertStringContainsString('2022-06-10T15:30:00', - $calendarRange->getStartDate()->format(\DateTimeImmutable::ATOM)); - $this->assertStringContainsString('2022-06-10T17:30:00', - $calendarRange->getEndDate()->format(\DateTimeImmutable::ATOM)); - $this->assertTrue($calendarRange->preventEnqueueChanges); - } - - public function testDeleteCalendarRangeWithoutAssociation(): void - { - $em = $this->prophesize(EntityManagerInterface::class); - $em->remove(Argument::type(CalendarRange::class))->shouldBeCalled(); - - $machineHttpClient = new MockHttpClient([ - new MockResponse(self::REMOTE_CALENDAR_RANGE, ['http_code' => 200]) - ]); - - $calendarRangeSyncer = new CalendarRangeSyncer( - $em->reveal(), - new NullLogger(), - $machineHttpClient - ); - - $calendarRange = new CalendarRange(); - $calendarRange - ->setUser($user = new User()) - ; - $notification = json_decode(self::NOTIF_DELETE, true); - - $calendarRangeSyncer->handleCalendarRangeSync( - $calendarRange, - $notification['value'][0], - $user); - $this->assertTrue($calendarRange->preventEnqueueChanges); - } + JSON; public function testDeleteCalendarRangeWithAssociation(): void { @@ -204,7 +153,7 @@ JSON; $em->remove(Argument::type(CalendarRange::class))->shouldNotBeCalled(); $machineHttpClient = new MockHttpClient([ - new MockResponse(self::REMOTE_CALENDAR_RANGE, ['http_code' => 200]) + new MockResponse(self::REMOTE_CALENDAR_RANGE, ['http_code' => 200]), ]); $calendarRangeSyncer = new CalendarRangeSyncer( @@ -215,8 +164,7 @@ JSON; $calendarRange = new CalendarRange(); $calendarRange - ->setUser($user = new User()) - ; + ->setUser($user = new User()); $calendar = new Calendar(); $calendar->setCalendarRange($calendarRange); @@ -225,7 +173,76 @@ JSON; $calendarRangeSyncer->handleCalendarRangeSync( $calendarRange, $notification['value'][0], - $user); + $user + ); + } + public function testDeleteCalendarRangeWithoutAssociation(): void + { + $em = $this->prophesize(EntityManagerInterface::class); + $em->remove(Argument::type(CalendarRange::class))->shouldBeCalled(); + + $machineHttpClient = new MockHttpClient([ + new MockResponse(self::REMOTE_CALENDAR_RANGE, ['http_code' => 200]), + ]); + + $calendarRangeSyncer = new CalendarRangeSyncer( + $em->reveal(), + new NullLogger(), + $machineHttpClient + ); + + $calendarRange = new CalendarRange(); + $calendarRange + ->setUser($user = new User()); + $notification = json_decode(self::NOTIF_DELETE, true); + + $calendarRangeSyncer->handleCalendarRangeSync( + $calendarRange, + $notification['value'][0], + $user + ); + $this->assertTrue($calendarRange->preventEnqueueChanges); + } + + public function testUpdateCalendarRange(): void + { + $em = $this->prophesize(EntityManagerInterface::class); + $machineHttpClient = new MockHttpClient([ + new MockResponse(self::REMOTE_CALENDAR_RANGE, ['http_code' => 200]), + ]); + + $calendarRangeSyncer = new CalendarRangeSyncer( + $em->reveal(), + new NullLogger(), + $machineHttpClient + ); + + $calendarRange = new CalendarRange(); + $calendarRange + ->setUser($user = new User()) + ->setStartDate(new DateTimeImmutable('2020-01-01 15:00:00')) + ->setEndDate(new DateTimeImmutable('2020-01-01 15:30:00')) + ->addRemoteAttributes([ + 'lastModifiedDateTime' => 0, + 'changeKey' => 'abc', + ]); + $notification = json_decode(self::NOTIF_UPDATE, true); + + $calendarRangeSyncer->handleCalendarRangeSync( + $calendarRange, + $notification['value'][0], + $user + ); + + $this->assertStringContainsString( + '2022-06-10T15:30:00', + $calendarRange->getStartDate()->format(DateTimeImmutable::ATOM) + ); + $this->assertStringContainsString( + '2022-06-10T17:30:00', + $calendarRange->getEndDate()->format(DateTimeImmutable::ATOM) + ); + $this->assertTrue($calendarRange->preventEnqueueChanges); } } diff --git a/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/CalendarSyncerTest.php b/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/CalendarSyncerTest.php index fb3da1ce6..f7f658b92 100644 --- a/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/CalendarSyncerTest.php +++ b/src/Bundle/ChillCalendarBundle/Tests/RemoteCalendar/Connector/MSGraph/CalendarSyncerTest.php @@ -1,154 +1,456 @@ \r\n
\r\n\r\n\r\n\r\n\r\n