Create a connector to synchronize with zimbra calendars

This commit is contained in:
2025-12-05 11:59:32 +00:00
parent 0ba5cd849c
commit 92d5fe154e
34 changed files with 1116 additions and 22 deletions

View File

@@ -0,0 +1,40 @@
# Chill Zimbra Bundle
This bundle provides integration with Zimbra email server for Chill application.
## Source code
This bundle should be modified within the chill-bundles repository. The code at
https://gitlab.com/Chill-Projet/chill-zimbra-connector is a mirror of the main
repository, and is intended to serve packagist information.
## Configuration
This bundle should be configured using a dsn scheme with a `zimbra+http` or `zimbra+https`
scheme.
```yaml
chill_calendar:
# remember to url-encode username and password
remote_calendar_dsn: zimbra+https://chill%40zimbra.example.com:password@zimbra.example.com
```
## Development
This bundles should be developed from within the chill-bundles repository.
During development, you must use an inline alias, or a branch alias to
be able to load the root package's master branch as a replacement for a version.
Example of composer.json for chill-project/chill-zimbra-bundle:
```json
{
"require": {
"chill-project/chill-bundles": "dev-master as v4.6.1",
"zimbra-api/soap-api": "^3.2.2",
"psr/http-client": "^1.0",
"nyholm/psr7": "^1.0"
}
}
```

View File

@@ -0,0 +1,22 @@
{
"name": "chill-project/chill-zimbra-bundle",
"description": "Provide connection between Zimbra agenda and Chill",
"minimum-stability": "stable",
"license": "AGPL-3.0",
"type": "library",
"keywords": [
"chill",
"social worker"
],
"require": {
"chill-project/chill-bundles": "dev-master as v4.6.1",
"zimbra-api/soap-api": "^3.2.2",
"psr/http-client": "^1.0",
"nyholm/psr7": "^1.0"
},
"autoload": {
"psr-4": {
"Chill\\ZimbraBundle\\": "src/"
}
}
}

View File

@@ -0,0 +1,147 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\ZimbraBundle\Calendar\Connector;
use Chill\CalendarBundle\Entity\Calendar;
use Chill\CalendarBundle\Entity\CalendarRange;
use Chill\CalendarBundle\Entity\Invite;
use Chill\CalendarBundle\RemoteCalendar\Connector\RemoteCalendarConnectorInterface;
use Chill\MainBundle\Entity\User;
use Chill\ZimbraBundle\Calendar\Connector\ZimbraConnector\CreateEvent;
use Chill\ZimbraBundle\Calendar\Connector\ZimbraConnector\DeleteEvent;
use Chill\ZimbraBundle\Calendar\Connector\ZimbraConnector\UpdateEvent;
use Psr\Log\LoggerInterface;
use Symfony\Component\HttpFoundation\Response;
final readonly class ZimbraConnector implements RemoteCalendarConnectorInterface
{
private const LOG_PREFIX = '[ZimbraConnector] ';
public function __construct(
private CreateEvent $createEvent,
private UpdateEvent $updateEvent,
private DeleteEvent $deleteEvent,
private LoggerInterface $logger,
) {}
public function countEventsForUser(User $user, \DateTimeImmutable $startDate, \DateTimeImmutable $endDate): int
{
return 0;
}
public function getMakeReadyResponse(string $returnPath): Response
{
throw new \BadMethodCallException('Zimbra connector is always ready');
}
public function isReady(): bool
{
return true;
}
public function listEventsForUser(User $user, \DateTimeImmutable $startDate, \DateTimeImmutable $endDate, ?int $offset = 0, ?int $limit = 50): array
{
return [];
}
public function removeCalendar(string $remoteId, array $remoteAttributes, User $user, ?CalendarRange $associatedCalendarRange = null): void
{
if ('' === $remoteId) {
return;
}
($this->deleteEvent)($user, $remoteId);
if (null !== $associatedCalendarRange) {
$this->logger->info(self::LOG_PREFIX.'Ask to re-create the previous calendar range', ['previous_calendar_range_id' => $associatedCalendarRange->getId()]);
$this->createCalendarRange($associatedCalendarRange);
}
}
public function removeCalendarRange(string $remoteId, array $remoteAttributes, User $user): void
{
if ('' === $remoteId) {
return;
}
($this->deleteEvent)($user, $remoteId);
}
public function syncCalendar(Calendar $calendar, string $action, ?CalendarRange $previousCalendarRange, ?User $previousMainUser, ?array $oldInvites, ?array $newInvites): void
{
if (null !== $previousMainUser && $previousMainUser !== $calendar->getMainUser()) {
$this->removeCalendar($calendar->getRemoteId(), [], $previousMainUser);
$calendar->setRemoteId('');
}
if (!$calendar->hasRemoteId()) {
$calItemId = ($this->createEvent)($calendar);
$this->logger->info(self::LOG_PREFIX.'Calendar synced with Zimbra', ['calendar_id' => $calendar->getId(), 'action' => $action, 'calItemId' => $calItemId]);
$calendar->setRemoteId($calItemId);
} else {
($this->updateEvent)($calendar);
$this->logger->info(self::LOG_PREFIX.'Calendar updated against zimbra', ['old_cal_remote_id' => $calendar->getRemoteId(), 'calendar_id' => $calendar->getId()]);
}
if (null !== $calendar->getCalendarRange()) {
$range = $calendar->getCalendarRange();
$this->removeCalendarRange($range->getRemoteId(), [], $range->getUser());
$range->setRemoteId('');
}
if (null !== $previousCalendarRange) {
$this->syncCalendarRange($previousCalendarRange);
}
foreach ($calendar->getInvites() as $invite) {
$this->syncInvite($invite);
}
}
public function syncCalendarRange(CalendarRange $calendarRange): void
{
if (!$calendarRange->hasRemoteId()) {
$this->createCalendarRange($calendarRange);
} else {
($this->updateEvent)($calendarRange);
$this->logger->info(self::LOG_PREFIX.'Calendar range updated against zimbra', ['old_cal_remote_id' => $calendarRange->getRemoteId(), 'calendar_range_id' => $calendarRange->getId()]);
}
}
public function syncInvite(Invite $invite): void
{
if (Invite::ACCEPTED === $invite->getStatus()) {
if ($invite->hasRemoteId()) {
($this->updateEvent)($invite);
$this->logger->info(self::LOG_PREFIX.'Invite range updated against zimbra', ['invite_id' => $invite->getId(), 'invite_remote_id', $invite->getRemoteId()]);
} else {
$remoteId = ($this->createEvent)($invite);
$invite->setRemoteId($remoteId);
$this->logger->info(self::LOG_PREFIX.'Invite range updated against zimbra', ['invite_id' => $invite->getId(), 'invite_remote_id', $invite->getRemoteId()]);
}
} elseif ($invite->hasRemoteId()) {
// case when the invite has been accepted in the past, and synchronized
($this->deleteEvent)($invite->getUser(), $invite->getRemoteId());
$this->logger->info(self::LOG_PREFIX.'Invite range removed in zimbra', ['invite_id' => $invite->getId(), 'invite_remote_id', $invite->getRemoteId()]);
$invite->setRemoteId('');
}
}
private function createCalendarRange(CalendarRange $calendarRange): void
{
$calItemId = ($this->createEvent)($calendarRange);
$this->logger->info(self::LOG_PREFIX.'Calendar range created with Zimbra', ['calendar_range_id' => $calendarRange->getId(), 'calItemId' => $calItemId]);
$calendarRange->setRemoteId($calItemId);
}
}

View File

@@ -0,0 +1,96 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\ZimbraBundle\Calendar\Connector\ZimbraConnector;
use Chill\CalendarBundle\Entity\Calendar;
use Chill\CalendarBundle\Entity\CalendarRange;
use Chill\CalendarBundle\Entity\Invite;
use Chill\ZimbraBundle\Exception\CalendarWithoutMainUserException;
use Symfony\Contracts\Translation\TranslatorInterface;
use Zimbra\Mail\Struct\InvitationInfo;
use Zimbra\Mail\Struct\InviteComponent;
use Zimbra\Mail\Struct\MimePartInfo;
use Zimbra\Mail\Struct\Msg;
/**
* Creates a new calendar event in Zimbra.
*
* This class handles the creation of new calendar events in the Zimbra system.
* It uses the Zimbra SOAP API to create events and returns a serialized ID that
* can be used as remoteId for Calendar and CalendarRange.
*/
final readonly class CreateEvent
{
public function __construct(
private SoapClientBuilder $soapClientBuilder,
private CreateZimbraComponent $createEvent,
private TranslatorInterface $translator,
private ZimbraIdSerializer $zimbraIdSerializer,
) {}
/**
* Creates a new calendar event in Zimbra.
*
* @param Calendar|CalendarRange|Invite $calendar The calendar event to create
*
* @return string The serialized Zimbra ID for the created event
*
* @throws CalendarWithoutMainUserException When the calendar has no associated user email
*/
public function __invoke(Calendar|CalendarRange|Invite $calendar): string
{
if ($calendar instanceof Calendar) {
$organizerEmail = $calendar->getMainUser()->getEmail();
$organizerLang = $calendar->getMainUser()->getLocale();
} elseif ($calendar instanceof Invite) {
$organizerEmail = $calendar->getUser()->getEmail();
$organizerLang = $calendar->getUser()->getLocale();
} else {
$organizerEmail = $calendar->getUser()->getEmail();
$organizerLang = $calendar->getUser()->getLocale();
}
if (null === $organizerEmail) {
throw new CalendarWithoutMainUserException();
}
$api = $this->soapClientBuilder->getApiForAccount($organizerEmail);
$comp = $this->createEvent->createZimbraInviteComponentFromCalendar($calendar);
$inv = new InvitationInfo();
$inv->setInviteComponent($comp);
$mp = new MimePartInfo();
$mp->addMimePart(new MimePartInfo('text/plain', $this->translator->trans('zimbra.event_created_by_chill', locale: $organizerLang)));
$msg = new Msg();
$msg->setSubject($this->translator->trans('zimbra.event_created_trough_soap', locale: $organizerLang))
->setFolderId('10')
->setInvite($inv)
->setMimePart($mp);
$response = $api->createAppointment($msg, echo: true);
$echo = $response->getEcho();
$invite = $echo->getInvite();
$MPInviteInfo = $invite->getInvite();
/** @var InviteComponent $firstInvite */
$firstInvite = $MPInviteInfo->getInviteComponents()[0];
return $this->zimbraIdSerializer->serializeId(
$response->getCalItemId(),
$response->getCalInvId(),
$firstInvite->getUid(),
);
}
}

View File

@@ -0,0 +1,129 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\ZimbraBundle\Calendar\Connector\ZimbraConnector;
use Chill\CalendarBundle\Entity\Calendar;
use Chill\CalendarBundle\Entity\CalendarRange;
use Chill\CalendarBundle\Entity\Invite;
use Chill\MainBundle\Entity\Location;
use Chill\MainBundle\Templating\Entity\AddressRender;
use Chill\PersonBundle\Entity\Person;
use Chill\PersonBundle\Templating\Entity\PersonRenderInterface;
use Symfony\Contracts\Translation\TranslatorInterface;
use Twig\Environment;
use Zimbra\Common\Enum\FreeBusyStatus;
use Zimbra\Common\Enum\InviteClass;
use Zimbra\Common\Enum\InviteStatus;
use Zimbra\Common\Enum\Transparency;
use Zimbra\Mail\Struct\InviteComponent;
/**
* Class responsible for creating Zimbra invite components based on calendar data.
*/
final readonly class CreateZimbraComponent
{
public function __construct(
private PersonRenderInterface $personRender,
private AddressRender $addressRender,
private DateConverter $dateConverter,
private TranslatorInterface $translator,
private Environment $twig,
) {}
/**
* Creates a Zimbra invite component from the provided calendar object.
*
* The method initializes a new InviteComponent object, sets its properties
* including name, free/busy status, status, classification, transparency,
* all-day and draft status, as well as start and end times. If the calendar
* contains a location, it also sets the location for the invite component.
*
* @param Calendar|CalendarRange|Invite $calendar a calendar object containing event data
*
* @return InviteComponent the configured Zimbra invite component
*/
public function createZimbraInviteComponentFromCalendar(Calendar|CalendarRange|Invite $calendar): InviteComponent
{
if ($calendar instanceof Calendar) {
$subject = '[Chill] '.
implode(
', ',
$calendar->getPersons()->map(fn (Person $p) => $this->personRender->renderString($p, ['addAge' => false]))->toArray()
);
$content = $this->twig->render('@ChillZimbra/ZimbraComponent/calendar_content.txt.twig', ['calendar' => $calendar]);
} elseif ($calendar instanceof Invite) {
$subject = '[Chill] '.
'('.$this->translator->trans('remote_calendar.calendar_invite_statement_in_calendar').') '.
implode(
', ',
$calendar->getCalendar()->getPersons()->map(fn (Person $p) => $this->personRender->renderString($p, ['addAge' => false]))->toArray()
);
$content = $this->twig->render('@ChillZimbra/ZimbraComponent/invitation_content.txt.twig', ['calendar' => $calendar->getCalendar()]);
} else {
// $calendar is an instanceof CalendarRange
$subject = $this->translator->trans('remote_calendar.calendar_range_title');
$content = '';
}
if ($calendar instanceof Invite) {
$startDate = $calendar->getCalendar()->getStartDate();
$endDate = $calendar->getCalendar()->getEndDate();
$location = $calendar->getCalendar()->getLocation();
$hasLocation = $calendar->getCalendar()->hasLocation();
$isPrivate = $calendar->getCalendar()->getAccompanyingPeriod()?->isConfidential() ?? false;
} else {
$startDate = $calendar->getStartDate();
$endDate = $calendar->getEndDate();
$location = $calendar->getLocation();
$hasLocation = $calendar->hasLocation();
$isPrivate = $calendar->getAccompanyingPeriod()?->isConfidential() ?? false;
}
$comp = new InviteComponent();
$comp->setName($subject);
$comp->setDescription($content);
$comp->setFreeBusy(FreeBusyStatus::BUSY);
$comp->setStatus(InviteStatus::CONFIRMED);
$comp->setCalClass($isPrivate ? InviteClass::PRI : InviteClass::PUB);
$comp->setTransparency(Transparency::OPAQUE);
$comp->setIsAllDay(false);
$comp->setIsDraft(false);
$comp->setDtStart($this->dateConverter->phpToZimbraDateTime($startDate));
$comp->setDtEnd($this->dateConverter->phpToZimbraDateTime($endDate));
if ($hasLocation) {
$comp
->setLocation($this->createLocationString($location));
}
return $comp;
}
private function createLocationString(Location $location): string
{
$str = '';
if ('' !== ($loc = (string) $location->getName())) {
$str .= $loc;
}
if ($location->hasAddress()) {
if ('' !== $str) {
$str .= ', ';
}
$str .= $this->addressRender->renderString($location->getAddress(), ['separator' => ', ']);
}
return $str;
}
}

View File

@@ -0,0 +1,37 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\ZimbraBundle\Calendar\Connector\ZimbraConnector;
use Zimbra\Mail\Struct\DtTimeInfo;
/**
* Class DateConverter.
*
* Provides methods for converting PHP DateTime objects
* into specific date-time formats or representations.
*/
final readonly class DateConverter
{
public const FORMAT_DATE_TIME = 'Ymd\THis';
/**
* Converts a PHP DateTimeInterface object into a Zimbra-specific DtTimeInfo object.
*
* @param \DateTimeInterface $date the date to be converted
*
* @return DtTimeInfo the converted DtTimeInfo object
*/
public function phpToZimbraDateTime(\DateTimeInterface $date): DtTimeInfo
{
return new DtTimeInfo($date->format(self::FORMAT_DATE_TIME), $date->getTimezone()->getName());
}
}

View File

@@ -0,0 +1,51 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\ZimbraBundle\Calendar\Connector\ZimbraConnector;
use Chill\MainBundle\Entity\User;
use Chill\ZimbraBundle\Exception\CalendarWithoutMainUserException;
/**
* Represents an action to delete an existing Appointment in the Zimbra calendar.
* The deletion is performed using the remoteId of the appointment.
*/
final readonly class DeleteEvent
{
public function __construct(
private SoapClientBuilder $soapClientBuilder,
private ZimbraIdSerializer $zimbraIdSerializer,
) {}
/**
* Delete an existing Appointment in the Zimbra calendar, from his remoteId.
*
* @param User $user The user who owns the calendar
* @param string $remoteId The remoteId of the appointment
*/
public function __invoke(User $user, string $remoteId): void
{
$organizerEmail = $user->getEmail();
if (null === $organizerEmail) {
throw new CalendarWithoutMainUserException();
}
$api = $this->soapClientBuilder->getApiForAccount($organizerEmail);
['calItemId' => $calItemId, 'calInvId' => $calInvId, 'inviteComponentCommonUid' => $inviteComponentCommonUid]
= $this->zimbraIdSerializer->deSerializeId($remoteId);
$api->cancelAppointment(
id: $calInvId,
componentNum: 0,
);
}
}

View File

@@ -0,0 +1,78 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\ZimbraBundle\Calendar\Connector\ZimbraConnector;
use Symfony\Component\DependencyInjection\ParameterBag\ParameterBagInterface;
use Symfony\Component\HttpClient\Psr18Client;
use Symfony\Contracts\HttpClient\HttpClientInterface;
use Zimbra\Common\Enum\AccountBy;
use Zimbra\Common\Soap\ClientFactory;
use Zimbra\Common\Struct\Header\AccountInfo;
use Zimbra\Mail\MailApi;
final readonly class SoapClientBuilder
{
private string $username;
private string $password;
private string $url;
public function __construct(private ParameterBagInterface $parameterBag, private HttpClientInterface $client)
{
$dsn = $this->parameterBag->get('chill_calendar.remote_calendar_dsn');
$url = parse_url($dsn);
$this->username = urldecode($url['user']);
$this->password = urldecode($url['pass']);
if ('zimbra+http' === $url['scheme']) {
$scheme = 'http://';
$port = $url['port'] ?? 80;
} elseif ('zimbra+https' === $url['scheme']) {
$scheme = 'https://';
$port = $url['port'] ?? 443;
} else {
throw new \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException('Unsupported remote calendar scheme: '.$url['scheme']);
}
$this->url = $scheme.$url['host'].':'.$port;
}
private function buildApi(): MailApi
{
$baseClient = $this->client->withOptions([
'base_uri' => $location = $this->url.'/service/soap',
'verify_host' => false,
'verify_peer' => false,
]);
$psr18Client = new Psr18Client($baseClient);
$api = new MailApi();
$client = ClientFactory::create($location, $psr18Client);
$api->setClient($client);
return $api;
}
public function getApiForAccount(string $accountName): MailApi
{
$api = $this->buildApi();
$response = $api->authByAccountName($this->username, $this->password);
$token = $response->getAuthToken();
$apiBy = $this->buildApi();
$apiBy->setAuthToken($token);
$apiBy->setTargetAccount(new AccountInfo(AccountBy::NAME, $accountName));
return $apiBy;
}
}

View File

@@ -0,0 +1,97 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\ZimbraBundle\Calendar\Connector\ZimbraConnector;
use Chill\CalendarBundle\Entity\Calendar;
use Chill\CalendarBundle\Entity\CalendarRange;
use Chill\CalendarBundle\Entity\Invite;
use Chill\ZimbraBundle\Exception\CalendarWithoutMainUserException;
use Symfony\Contracts\Translation\TranslatorInterface;
use Zimbra\Mail\Struct\InvitationInfo;
use Zimbra\Mail\Struct\MimePartInfo;
use Zimbra\Mail\Struct\Msg;
/**
* Updates an existing calendar event in Zimbra.
*
* This class handles the modification of existing calendar events in the Zimbra system.
* It uses the Zimbra SOAP API to update event details while maintaining the original
* event's metadata like IDs and sequences.
*/
final readonly class UpdateEvent
{
public function __construct(
private SoapClientBuilder $soapClientBuilder,
private CreateZimbraComponent $createZimbraComponent,
private TranslatorInterface $translator,
private ZimbraIdSerializer $zimbraIdSerializer,
) {}
/**
* Updates an existing calendar event in Zimbra.
*
* @param Calendar|CalendarRange $calendar The calendar event to update
*
* @throws CalendarWithoutMainUserException When the calendar has no associated user email
*/
public function __invoke(Calendar|CalendarRange|Invite $calendar): void
{
if ($calendar instanceof Calendar) {
$organizerEmail = $calendar->getMainUser()->getEmail();
$organizerLang = $calendar->getMainUser()->getLocale();
} elseif ($calendar instanceof Invite) {
$organizerEmail = $calendar->getCalendar()->getMainUser()->getEmail();
$organizerLang = $calendar->getCalendar()->getMainUser()->getLocale();
} else {
$organizerEmail = $calendar->getUser()->getEmail();
$organizerLang = $calendar->getUser()->getLocale();
}
if (null === $organizerEmail) {
throw new CalendarWithoutMainUserException();
}
$api = $this->soapClientBuilder->getApiForAccount($organizerEmail);
['calItemId' => $calItemId, 'calInvId' => $calInvId, 'inviteComponentCommonUid' => $inviteComponentCommonUid]
= $this->zimbraIdSerializer->deSerializeId($calendar->getRemoteId());
$existing = $api->getAppointment(sync: true, includeContent: true, includeInvites: true, id: $calItemId);
$appt = $existing->getApptItem();
$comp = $this->createZimbraComponent->createZimbraInviteComponentFromCalendar($calendar);
$comp->setUid($inviteComponentCommonUid);
$inv = new InvitationInfo();
$inv->setInviteComponent($comp)
->setUid($calInvId);
$mp = new MimePartInfo();
$mp->addMimePart(new MimePartInfo('text/plain', $this->translator->trans('zimbra.event_created_by_chill', locale: $organizerLang)));
$msg = new Msg();
$msg->setSubject($this->translator->trans('zimbra.event_created_trough_soap', locale: $organizerLang))
->setFolderId('10')
->setInvite($inv)
->setMimePart($mp)
;
$response = $api->modifyAppointment(
id: $calInvId,
componentNum: 0,
modifiedSequence: $appt->getModifiedSequence(),
revision: $appt->getRevision(),
msg: $msg,
echo: true
);
}
}

View File

@@ -0,0 +1,67 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\ZimbraBundle\Calendar\Connector\ZimbraConnector;
use Chill\ZimbraBundle\Exception\ZimbraCalendarIdNotDeserializedException;
/**
* Serializes and deserializes Zimbra calendar event IDs.
*
* This class handles the conversion between Zimbra's individual ID components
* and a single serialized string format, allowing for consistent storage and retrieval
* of Zimbra calendar event identifiers.
*/
final readonly class ZimbraIdSerializer
{
/**
* Serializes individual Zimbra calendar ID components into a single string.
*
* @param string $calItemId The calendar item ID from Zimbra
* @param string $calInvId The calendar invitation ID from Zimbra
* @param string $inviteComponentCommonUid The common UID for the invite component
*
* @return string The serialized ID in format "calItemId|calInvId|inviteComponentCommonUid|v0"
*/
public function serializeId(string $calItemId, string $calInvId, string $inviteComponentCommonUid): string
{
return sprintf(
'%s|%s|%s|v0',
$calItemId,
$calInvId,
$inviteComponentCommonUid,
);
}
/**
* Deserializes a Zimbra calendar ID string into its component parts.
*
* @param string $remoteId The serialized ID, as stored in the remoteId's Calendar or CalendarRange
*
* @return array{calItemId: string, calInvId: string, inviteComponentCommonUid: string} Associative array containing the ID components
*
* @throws ZimbraCalendarIdNotDeserializedException If the remote ID format is invalid or incompatible
*/
public function deSerializeId(string $remoteId): array
{
if (!str_ends_with($remoteId, 'v0')) {
throw new ZimbraCalendarIdNotDeserializedException();
}
$exploded = explode('|', $remoteId);
return [
'calItemId' => $exploded[0],
'calInvId' => $exploded[1],
'inviteComponentCommonUid' => $exploded[2],
];
}
}

View File

@@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\ZimbraBundle;
use Symfony\Component\HttpKernel\Bundle\Bundle;
class ChillZimbraBundle extends Bundle {}

View File

@@ -0,0 +1,26 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\ZimbraBundle\DependencyInjection;
use Symfony\Component\Config\FileLocator;
use Symfony\Component\DependencyInjection\ContainerBuilder;
use Symfony\Component\DependencyInjection\Extension\Extension;
use Symfony\Component\DependencyInjection\Loader\YamlFileLoader;
class ChillZimbraExtension extends Extension
{
public function load(array $configs, ContainerBuilder $container)
{
$loader = new YamlFileLoader($container, new FileLocator(__DIR__.'/../config'));
$loader->load('services.yaml');
}
}

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\ZimbraBundle\Exception;
class CalendarWithoutMainUserException extends \RuntimeException {}

View File

@@ -0,0 +1,14 @@
<?php
declare(strict_types=1);
/*
* Chill is a software for social workers
*
* For the full copyright and license information, please view
* the LICENSE file that was distributed with this source code.
*/
namespace Chill\ZimbraBundle\Exception;
class ZimbraCalendarIdNotDeserializedException extends \RuntimeException {}

View File

@@ -0,0 +1,7 @@
services:
_defaults:
autoconfigure: true
autowire: true
Chill\ZimbraBundle\Calendar\:
resource: '../Calendar'

View File

@@ -0,0 +1,18 @@
{{ 'zimbra.content.appointment_created_by_chill'|trans }}
{{ 'zimbra.content.link_to_edit'|trans }} : {{ absolute_url(path('chill_calendar_calendar_edit', {'id': calendar.id, '_locale': calendar.mainUser.locale })) }}
{{ calendar.comment|chill_entity_render_string }}
{{ 'zimbra.content.persons_and_professionnals_concerned'|trans }} :
{% for e in calendar.persons %}
- {{ e|chill_entity_render_string({'addAge': false}) }}
{% endfor -%}
{%- for e in calendar.professionals %}
- {{ e|chill_entity_render_string }}
{% endfor -%}
{%- for e in calendar.users %}
- {{ e|chill_entity_render_string }}
{% endfor %}

View File

@@ -0,0 +1,20 @@
{{ 'zimbra.content.appointment_created_by_chill'|trans }}
{{ 'zimbra.content.you_were_invited_by'|trans }}: {{ calendar.mainUser|chill_entity_render_string }}
{{ 'zimbra.content.link_to_edit'|trans }} : {{ absolute_url(path('chill_calendar_calendar_edit', {'id': calendar.id, '_locale': calendar.mainUser.locale })) }}
{{ calendar.comment|chill_entity_render_string }}
{{ 'zimbra.content.persons_and_professionnals_concerned'|trans }} :
{% for e in calendar.persons %}
- {{ e|chill_entity_render_string({'addAge': false}) }}
{% endfor -%}
{%- for e in calendar.professionals %}
- {{ e|chill_entity_render_string }}
{% endfor -%}
{%- for e in calendar.users %}
- {{ e|chill_entity_render_string }}
{% endfor %}

View File

@@ -0,0 +1,9 @@
zimbra:
event_created_by_chill: Événement créé par Chill
event_created_trough_soap: Événement créé via l'API SOAP
content:
appointment_created_by_chill: Ce rendez-vous a été créé par Chill
link_to_edit: Pour modifier ou supprimer ce rendez-vous, utilisez ladresse suivante
persons_and_professionnals_concerned: Usagers et tiers concernés
you_were_invited_by: Vous avez été invité par