mirror of
https://gitlab.com/Chill-Projet/chill-bundles.git
synced 2025-12-06 18:33:55 +00:00
Create a connector to synchronize with zimbra calendars
This commit is contained in:
@@ -38,15 +38,67 @@ variables:
|
|||||||
TZ: Europe/Brussels
|
TZ: Europe/Brussels
|
||||||
# avoid direct deprecations (using symfony phpunit bridge: https://symfony.com/doc/4.x/components/phpunit_bridge.html#internal-deprecations
|
# avoid direct deprecations (using symfony phpunit bridge: https://symfony.com/doc/4.x/components/phpunit_bridge.html#internal-deprecations
|
||||||
SYMFONY_DEPRECATIONS_HELPER: max[total]=99999999&max[self]=0&max[direct]=45&verbose=0
|
SYMFONY_DEPRECATIONS_HELPER: max[total]=99999999&max[self]=0&max[direct]=45&verbose=0
|
||||||
|
# consider the root package at the dev-master version
|
||||||
|
# this is required to work with packages
|
||||||
|
# see https://getcomposer.org/doc/articles/troubleshooting.md#dependencies-on-the-root-package
|
||||||
|
COMPOSER_ROOT_VERSION: dev-master
|
||||||
|
|
||||||
stages:
|
stages:
|
||||||
|
- mirror
|
||||||
- Composer install
|
- Composer install
|
||||||
- Tests
|
- Tests
|
||||||
- Deploy
|
- Deploy
|
||||||
|
|
||||||
|
mirror_chill_zimbra_bundle:
|
||||||
|
stage: mirror
|
||||||
|
image: alpine:latest
|
||||||
|
|
||||||
|
variables:
|
||||||
|
GIT_DEPTH: 0 # <-- access to the full git history
|
||||||
|
|
||||||
|
rules:
|
||||||
|
# 1) Allow manual run from GitLab UI, whatever the branch
|
||||||
|
- if: '$CI_PIPELINE_SOURCE == "web"'
|
||||||
|
|
||||||
|
# 2) Auto-run on commits to master or 472-zimbra-connector
|
||||||
|
# but only if relevant files changed
|
||||||
|
- if: '$CI_COMMIT_BRANCH == "master" || $CI_COMMIT_BRANCH == "472-zimbra-connector"'
|
||||||
|
changes:
|
||||||
|
- packages/ChillZimbraBundle/**/*
|
||||||
|
- .gitlab-ci.yml
|
||||||
|
|
||||||
|
# 3) Otherwise: never run
|
||||||
|
- when: never
|
||||||
|
|
||||||
|
|
||||||
|
before_script:
|
||||||
|
- apk add --no-cache git git-subtree openssh
|
||||||
|
# Config git
|
||||||
|
- git config --global user.email "ci@gitlab.com"
|
||||||
|
- git config --global user.name "GitLab CI"
|
||||||
|
# Préparation SSH
|
||||||
|
- mkdir -p ~/.ssh
|
||||||
|
- cp "$DEPLOY_KEY" ~/.ssh/id_ed25519
|
||||||
|
- printf '\n' >> ~/.ssh/id_ed25519
|
||||||
|
- chmod 600 ~/.ssh/id_ed25519
|
||||||
|
- ssh-keyscan gitlab.com >> ~/.ssh/known_hosts
|
||||||
|
# Ajout du remote vers le repo dédié
|
||||||
|
- git remote add chill-zimbra-connector git@gitlab.com:Chill-Projet/chill-zimbra-connector.git || true
|
||||||
|
|
||||||
|
script:
|
||||||
|
# On s'assure d'être sur la bonne branche (celle qui a déclenché le job, master)
|
||||||
|
- git checkout "$CI_COMMIT_REF_NAME"
|
||||||
|
|
||||||
|
# Crée une branche temporaire qui contient uniquement l'historique de packages/ChillZimbraBundle
|
||||||
|
- git subtree split --prefix=packages/ChillZimbraBundle -b chill_zimbra_temp
|
||||||
|
|
||||||
|
# Push vers le repo cible, branche master du repo chill-zimbra-connector
|
||||||
|
- git push --force chill-zimbra-connector chill_zimbra_temp:main
|
||||||
|
|
||||||
build:
|
build:
|
||||||
stage: Composer install
|
stage: Composer install
|
||||||
image: chill/base-image:8.3-edge
|
image: chill/base-image:8.3-edge
|
||||||
|
variables:
|
||||||
before_script:
|
before_script:
|
||||||
- composer config -g cache-dir "$(pwd)/.cache"
|
- composer config -g cache-dir "$(pwd)/.cache"
|
||||||
script:
|
script:
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ $finder = PhpCsFixer\Finder::create();
|
|||||||
$finder
|
$finder
|
||||||
->in(__DIR__.'/src')
|
->in(__DIR__.'/src')
|
||||||
->in(__DIR__.'/utils')
|
->in(__DIR__.'/utils')
|
||||||
|
->in(__DIR__.'/packages')
|
||||||
->append([__FILE__])
|
->append([__FILE__])
|
||||||
->exclude(['docs/', 'tests/app'])
|
->exclude(['docs/', 'tests/app'])
|
||||||
->notPath('tests/app')
|
->notPath('tests/app')
|
||||||
|
|||||||
@@ -7,6 +7,13 @@
|
|||||||
"chill",
|
"chill",
|
||||||
"social worker"
|
"social worker"
|
||||||
],
|
],
|
||||||
|
"repositories": [{
|
||||||
|
"type": "path",
|
||||||
|
"url": "./packages/ChillZimbraBundle",
|
||||||
|
"options": {
|
||||||
|
"symlink": true
|
||||||
|
}
|
||||||
|
}],
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.2",
|
"php": "^8.2",
|
||||||
"ext-dom": "*",
|
"ext-dom": "*",
|
||||||
@@ -14,6 +21,7 @@
|
|||||||
"ext-openssl": "*",
|
"ext-openssl": "*",
|
||||||
"ext-redis": "*",
|
"ext-redis": "*",
|
||||||
"ext-zlib": "*",
|
"ext-zlib": "*",
|
||||||
|
"chill-project/chill-zimbra-bundle": "@dev",
|
||||||
"champs-libres/wopi-bundle": "dev-symfony-v5@dev",
|
"champs-libres/wopi-bundle": "dev-symfony-v5@dev",
|
||||||
"champs-libres/wopi-lib": "dev-master@dev",
|
"champs-libres/wopi-lib": "dev-master@dev",
|
||||||
"doctrine/data-fixtures": "^1.8",
|
"doctrine/data-fixtures": "^1.8",
|
||||||
|
|||||||
@@ -37,4 +37,5 @@ return [
|
|||||||
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
|
Symfony\Bundle\WebProfilerBundle\WebProfilerBundle::class => ['dev' => true, 'test' => true],
|
||||||
Symfony\UX\Translator\UxTranslatorBundle::class => ['all' => true],
|
Symfony\UX\Translator\UxTranslatorBundle::class => ['all' => true],
|
||||||
loophp\PsrHttpMessageBridgeBundle\PsrHttpMessageBridgeBundle::class => ['all' => true],
|
loophp\PsrHttpMessageBridgeBundle\PsrHttpMessageBridgeBundle::class => ['all' => true],
|
||||||
|
Chill\ZimbraBundle\ChillZimbraBundle::class => ['all' => true],
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -11,6 +11,7 @@
|
|||||||
"@hotwired/stimulus": "^3.0.0",
|
"@hotwired/stimulus": "^3.0.0",
|
||||||
"@luminateone/eslint-baseline": "^1.0.9",
|
"@luminateone/eslint-baseline": "^1.0.9",
|
||||||
"@symfony/stimulus-bridge": "^3.2.0",
|
"@symfony/stimulus-bridge": "^3.2.0",
|
||||||
|
"@symfony/ux-translator": "file:vendor/symfony/ux-translator/assets",
|
||||||
"@symfony/webpack-encore": "^4.1.0",
|
"@symfony/webpack-encore": "^4.1.0",
|
||||||
"@tsconfig/node20": "^20.1.4",
|
"@tsconfig/node20": "^20.1.4",
|
||||||
"@types/dompurify": "^3.0.5",
|
"@types/dompurify": "^3.0.5",
|
||||||
|
|||||||
40
packages/ChillZimbraBundle/README.md
Normal file
40
packages/ChillZimbraBundle/README.md
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
22
packages/ChillZimbraBundle/composer.json
Normal file
22
packages/ChillZimbraBundle/composer.json
Normal 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/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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],
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
16
packages/ChillZimbraBundle/src/ChillZimbraBundle.php
Normal file
16
packages/ChillZimbraBundle/src/ChillZimbraBundle.php
Normal 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 {}
|
||||||
@@ -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');
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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 {}
|
||||||
@@ -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 {}
|
||||||
7
packages/ChillZimbraBundle/src/config/services.yaml
Normal file
7
packages/ChillZimbraBundle/src/config/services.yaml
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
services:
|
||||||
|
_defaults:
|
||||||
|
autoconfigure: true
|
||||||
|
autowire: true
|
||||||
|
|
||||||
|
Chill\ZimbraBundle\Calendar\:
|
||||||
|
resource: '../Calendar'
|
||||||
@@ -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 %}
|
||||||
|
|
||||||
@@ -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 %}
|
||||||
|
|
||||||
@@ -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 l’adresse suivante
|
||||||
|
persons_and_professionnals_concerned: Usagers et tiers concernés
|
||||||
|
you_were_invited_by: Vous avez été invité par
|
||||||
@@ -3,6 +3,7 @@ parameters:
|
|||||||
paths:
|
paths:
|
||||||
- src/
|
- src/
|
||||||
- utils/
|
- utils/
|
||||||
|
- packages/
|
||||||
tmpDir: var/cache/phpstan
|
tmpDir: var/cache/phpstan
|
||||||
reportUnmatchedIgnoredErrors: false
|
reportUnmatchedIgnoredErrors: false
|
||||||
excludePaths:
|
excludePaths:
|
||||||
|
|||||||
@@ -47,6 +47,8 @@ class ChillCalendarExtension extends Extension implements PrependExtensionInterf
|
|||||||
} else {
|
} else {
|
||||||
$container->setParameter('chill_calendar.short_messages', null);
|
$container->setParameter('chill_calendar.short_messages', null);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$container->setParameter('chill_calendar.remote_calendar_dsn', $config['remote_calendar_dsn']);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function prepend(ContainerBuilder $container)
|
public function prepend(ContainerBuilder $container)
|
||||||
|
|||||||
@@ -32,9 +32,10 @@ class Configuration implements ConfigurationInterface
|
|||||||
->canBeDisabled()
|
->canBeDisabled()
|
||||||
->children()->end()
|
->children()->end()
|
||||||
->end() // end for short_messages
|
->end() // end for short_messages
|
||||||
|
->scalarNode('remote_calendar_dsn')->defaultValue('null://null')->cannotBeEmpty()->end()
|
||||||
->arrayNode('remote_calendars_sync')->canBeEnabled()
|
->arrayNode('remote_calendars_sync')->canBeEnabled()
|
||||||
->children()
|
->children()
|
||||||
->arrayNode('microsoft_graph')->canBeEnabled()
|
->arrayNode('microsoft_graph')->canBeEnabled()->setDeprecated('chill-project/chill-bundles', '4.7.0', 'The child node %node% at path %path% is deprecated: use remote_calendar_dsn instead, with a "msgraph://default" value')
|
||||||
->children()
|
->children()
|
||||||
->end() // end of machine_access_token
|
->end() // end of machine_access_token
|
||||||
->end() // end of microsoft_graph children
|
->end() // end of microsoft_graph children
|
||||||
|
|||||||
@@ -107,6 +107,11 @@ class CalendarRange implements TrackCreationInterface, TrackUpdateInterface
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function hasLocation(): bool
|
||||||
|
{
|
||||||
|
return null !== $this->location;
|
||||||
|
}
|
||||||
|
|
||||||
public function setLocation(?Location $location): self
|
public function setLocation(?Location $location): self
|
||||||
{
|
{
|
||||||
$this->location = $location;
|
$this->location = $location;
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ use Chill\CalendarBundle\Messenger\Message\CalendarRemovedMessage;
|
|||||||
use Chill\CalendarBundle\RemoteCalendar\Connector\RemoteCalendarConnectorInterface;
|
use Chill\CalendarBundle\RemoteCalendar\Connector\RemoteCalendarConnectorInterface;
|
||||||
use Chill\CalendarBundle\Repository\CalendarRangeRepository;
|
use Chill\CalendarBundle\Repository\CalendarRangeRepository;
|
||||||
use Chill\MainBundle\Repository\UserRepositoryInterface;
|
use Chill\MainBundle\Repository\UserRepositoryInterface;
|
||||||
|
use Doctrine\ORM\EntityManagerInterface;
|
||||||
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
|
use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -31,7 +32,12 @@ use Symfony\Component\Messenger\Handler\MessageHandlerInterface;
|
|||||||
*/
|
*/
|
||||||
class CalendarRemoveHandler implements MessageHandlerInterface
|
class CalendarRemoveHandler implements MessageHandlerInterface
|
||||||
{
|
{
|
||||||
public function __construct(private readonly RemoteCalendarConnectorInterface $remoteCalendarConnector, private readonly CalendarRangeRepository $calendarRangeRepository, private readonly UserRepositoryInterface $userRepository) {}
|
public function __construct(
|
||||||
|
private readonly RemoteCalendarConnectorInterface $remoteCalendarConnector,
|
||||||
|
private readonly CalendarRangeRepository $calendarRangeRepository,
|
||||||
|
private readonly UserRepositoryInterface $userRepository,
|
||||||
|
private readonly EntityManagerInterface $entityManager,
|
||||||
|
) {}
|
||||||
|
|
||||||
public function __invoke(CalendarRemovedMessage $message)
|
public function __invoke(CalendarRemovedMessage $message)
|
||||||
{
|
{
|
||||||
@@ -47,5 +53,7 @@ class CalendarRemoveHandler implements MessageHandlerInterface
|
|||||||
$this->userRepository->find($message->getCalendarUserId()),
|
$this->userRepository->find($message->getCalendarUserId()),
|
||||||
$associatedRange
|
$associatedRange
|
||||||
);
|
);
|
||||||
|
|
||||||
|
$this->entityManager->flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,42 +21,137 @@ namespace Chill\CalendarBundle\RemoteCalendar\Connector;
|
|||||||
use Chill\CalendarBundle\Entity\Calendar;
|
use Chill\CalendarBundle\Entity\Calendar;
|
||||||
use Chill\CalendarBundle\Entity\CalendarRange;
|
use Chill\CalendarBundle\Entity\CalendarRange;
|
||||||
use Chill\CalendarBundle\Entity\Invite;
|
use Chill\CalendarBundle\Entity\Invite;
|
||||||
|
use Chill\CalendarBundle\Messenger\Message\CalendarMessage;
|
||||||
use Chill\CalendarBundle\RemoteCalendar\Model\RemoteEvent;
|
use Chill\CalendarBundle\RemoteCalendar\Model\RemoteEvent;
|
||||||
use Chill\MainBundle\Entity\User;
|
use Chill\MainBundle\Entity\User;
|
||||||
use Symfony\Component\HttpFoundation\Response;
|
use Symfony\Component\HttpFoundation\Response;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Contract for connectors that synchronize Chill calendars with a remote
|
||||||
|
* calendar provider (for example Microsoft 365/Graph, Zimbra, ...).
|
||||||
|
*
|
||||||
|
* Implementations act as an adapter between Chill domain objects
|
||||||
|
* (Calendar, CalendarRange, Invite) and the remote provider API. They must:
|
||||||
|
* - expose a readiness flow for per-user authorization when applicable
|
||||||
|
* (see {@see getMakeReadyResponse()} and {@see isReady()});
|
||||||
|
* - list and count remote events in a time range for a given user;
|
||||||
|
* - mirror local lifecycle changes to the remote provider for calendars,
|
||||||
|
* calendar ranges (availability/busy blocks) and invites/attendees.
|
||||||
|
*
|
||||||
|
* Use {@see MSGraphRemoteCalendarConnector} as a reference implementation for
|
||||||
|
* expected behaviours, error handling and parameter semantics.
|
||||||
|
*/
|
||||||
interface RemoteCalendarConnectorInterface
|
interface RemoteCalendarConnectorInterface
|
||||||
{
|
{
|
||||||
public function countEventsForUser(User $user, \DateTimeImmutable $startDate, \DateTimeImmutable $endDate): int;
|
public function countEventsForUser(User $user, \DateTimeImmutable $startDate, \DateTimeImmutable $endDate): int;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return a response, more probably a RedirectResponse, where the user
|
* Returns a Response (typically a RedirectResponse) that lets the current
|
||||||
* will be able to fullfill requirements to prepare this connector and
|
* user perform the steps required to make the connector usable (for
|
||||||
* make it ready.
|
* example, OAuth consent or account linking). After completion, the user
|
||||||
|
* should be redirected back to the given path.
|
||||||
*/
|
*/
|
||||||
public function getMakeReadyResponse(string $returnPath): Response;
|
public function getMakeReadyResponse(string $returnPath): Response;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Return true if the connector is ready to act as a proxy for reading
|
* Returns true when the connector is ready to access the remote provider
|
||||||
* remote calendars.
|
* on behalf of the current user (e.g. required tokens/consent exist).
|
||||||
*/
|
*/
|
||||||
public function isReady(): bool;
|
public function isReady(): bool;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Lists events from the remote provider for the given user and time range.
|
||||||
|
*
|
||||||
|
* Implementations should map provider-specific payloads to instances of
|
||||||
|
* {@see RemoteEvent}.
|
||||||
|
*
|
||||||
* @return array|RemoteEvent[]
|
* @return array|RemoteEvent[]
|
||||||
*/
|
*/
|
||||||
public function listEventsForUser(User $user, \DateTimeImmutable $startDate, \DateTimeImmutable $endDate, ?int $offset = 0, ?int $limit = 50): array;
|
public function listEventsForUser(User $user, \DateTimeImmutable $startDate, \DateTimeImmutable $endDate, ?int $offset = 0, ?int $limit = 50): array;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a calendar (single event) from the remote provider.
|
||||||
|
*
|
||||||
|
* **Note**: calendar (single event) which are canceled will appears in this
|
||||||
|
* method, and not in syncCalendar method.
|
||||||
|
*
|
||||||
|
* Parameters:
|
||||||
|
* - remoteId: the provider identifier of the remote event to delete. If
|
||||||
|
* empty, implementations should no-op.
|
||||||
|
* - remoteAttributes: provider-specific metadata previously stored with the
|
||||||
|
* local entity (e.g. change keys, etags) that can help perform safe
|
||||||
|
* concurrency checks when deleting. Implementations may ignore unknown
|
||||||
|
* keys.
|
||||||
|
* - user: the user in whose remote calendar the event lives and on whose
|
||||||
|
* behalf the deletion must be performed.
|
||||||
|
* - associatedCalendarRange: when provided, the implementation should
|
||||||
|
* update/synchronize the corresponding remote busy-time block after the
|
||||||
|
* event removal so that availability stays consistent.
|
||||||
|
*/
|
||||||
public function removeCalendar(string $remoteId, array $remoteAttributes, User $user, ?CalendarRange $associatedCalendarRange = null): void;
|
public function removeCalendar(string $remoteId, array $remoteAttributes, User $user, ?CalendarRange $associatedCalendarRange = null): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a remote busy-time block (calendar range) identified by
|
||||||
|
* provider-specific id and attributes for the given user.
|
||||||
|
*
|
||||||
|
* Implementations should no-op if the id is empty.
|
||||||
|
*/
|
||||||
public function removeCalendarRange(string $remoteId, array $remoteAttributes, User $user): void;
|
public function removeCalendarRange(string $remoteId, array $remoteAttributes, User $user): void;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
* Synchronizes a Calendar entity to the remote provider.
|
||||||
|
*
|
||||||
|
* Typical cases to support (see MSGraph implementation):
|
||||||
|
* - Creating the event on the remote calendar when it has no remote id.
|
||||||
|
* - Updating the existing remote event when details or attendees change.
|
||||||
|
* - Handling main user changes: cancel on the previous user's calendar,
|
||||||
|
* (re)create associated ranges where needed, then create on the new
|
||||||
|
* main user's calendar.
|
||||||
|
* - If the Calendar uses a CalendarRange that already exists remotely,
|
||||||
|
* implementations should remove/update that remote range when the event
|
||||||
|
* becomes the source of truth for busy times.
|
||||||
|
*
|
||||||
|
* The implementation should not expects to receive calendar which are canceled
|
||||||
|
* here.
|
||||||
|
*
|
||||||
|
* Parameters:
|
||||||
|
* - calendar: the domain Calendar to mirror remotely.
|
||||||
|
* - action: a hint about what triggered the sync; implementations should not rely
|
||||||
|
* solely on this value and must base decisions on the Calendar state.
|
||||||
|
* - previousCalendarRange: if the Calendar was previously attached to a
|
||||||
|
* different range, this contains the former range so it can be recreated
|
||||||
|
* remotely to preserve availability history when applicable.
|
||||||
|
* - previousMainUser: the former main user, when the main user changed;
|
||||||
|
* used to cancel the event in the previous user's calendar.
|
||||||
|
* - oldInvites: the attendee snapshot before the change. Each item is an
|
||||||
|
* array with keys: inviteId, userId, userEmail, userLabel.
|
||||||
|
* - newInvites: the attendee snapshot after the change, same shape as
|
||||||
|
* oldInvites. Implementations can compute diffs to add/remove attendees.
|
||||||
|
*
|
||||||
|
* The $action argument is a string tag indicating what happened to the
|
||||||
|
* calendar. It MUST be one of the constants defined on
|
||||||
|
* {@see CalendarMessage}:
|
||||||
|
* - {@see CalendarMessage::CALENDAR_PERSIST}
|
||||||
|
* - {@see CalendarMessage::CALENDAR_UPDATE}
|
||||||
|
*
|
||||||
* @param array<array{inviteId: int, userId: int, userEmail: int, userLabel: string}> $oldInvites
|
* @param array<array{inviteId: int, userId: int, userEmail: int, userLabel: string}> $oldInvites
|
||||||
|
*
|
||||||
|
* @phpstan-param (CalendarMessage::CALENDAR_PERSIST|CalendarMessage::CALENDAR_UPDATE) $action
|
||||||
*/
|
*/
|
||||||
public function syncCalendar(Calendar $calendar, string $action, ?CalendarRange $previousCalendarRange, ?User $previousMainUser, ?array $oldInvites, ?array $newInvites): void;
|
public function syncCalendar(Calendar $calendar, string $action, ?CalendarRange $previousCalendarRange, ?User $previousMainUser, ?array $oldInvites, ?array $newInvites): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates or updates a remote busy-time block representing the provided
|
||||||
|
* CalendarRange. If the range has a remote id, it should be updated;
|
||||||
|
* otherwise it should be created remotely, and the range enriched with
|
||||||
|
* the new id/attributes by the caller.
|
||||||
|
*/
|
||||||
public function syncCalendarRange(CalendarRange $calendarRange): void;
|
public function syncCalendarRange(CalendarRange $calendarRange): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Synchronizes a single Invite (attendee) change to the remote provider.
|
||||||
|
* Implementations may need to lookup the attendee's personal calendar to
|
||||||
|
* find provider-specific identifiers before patching the main event.
|
||||||
|
*/
|
||||||
public function syncInvite(Invite $invite): void;
|
public function syncInvite(Invite $invite): void;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,25 +35,46 @@ use TheNetworg\OAuth2\Client\Provider\Azure;
|
|||||||
|
|
||||||
class RemoteCalendarCompilerPass implements CompilerPassInterface
|
class RemoteCalendarCompilerPass implements CompilerPassInterface
|
||||||
{
|
{
|
||||||
|
private const ZIMBRA_CONNECTOR = 'Chill\ZimbraBundle\Calendar\Connector\ZimbraConnector';
|
||||||
|
|
||||||
|
private const MS_GRAPH_SERVICES_TO_REMOVE = [
|
||||||
|
MapAndSubscribeUserCalendarCommand::class,
|
||||||
|
AzureGrantAdminConsentAndAcquireToken::class,
|
||||||
|
RemoteCalendarConnectAzureController::class,
|
||||||
|
MachineTokenStorage::class,
|
||||||
|
MachineHttpClient::class,
|
||||||
|
MSGraphRemoteCalendarConnector::class,
|
||||||
|
MSUserAbsenceReaderInterface::class,
|
||||||
|
MSUserAbsenceSync::class,
|
||||||
|
];
|
||||||
|
|
||||||
public function process(ContainerBuilder $container)
|
public function process(ContainerBuilder $container)
|
||||||
{
|
{
|
||||||
$config = $container->getParameter('chill_calendar');
|
$config = $container->getParameter('chill_calendar.remote_calendar_dsn');
|
||||||
|
if (true === $container->getParameter('chill_calendar')['remote_calendars_sync']['microsoft_graph']['enabled']) {
|
||||||
|
$dsn = 'msgraph://default';
|
||||||
|
} else {
|
||||||
|
$dsn = $config;
|
||||||
|
}
|
||||||
|
|
||||||
if (true === $config['remote_calendars_sync']['microsoft_graph']['enabled']) {
|
$scheme = parse_url($dsn, PHP_URL_SCHEME);
|
||||||
|
|
||||||
|
if ('msgraph' === $scheme) {
|
||||||
$connector = MSGraphRemoteCalendarConnector::class;
|
$connector = MSGraphRemoteCalendarConnector::class;
|
||||||
|
|
||||||
$container->setAlias(HttpClientInterface::class.' $machineHttpClient', MachineHttpClient::class);
|
$container->setAlias(HttpClientInterface::class.' $machineHttpClient', MachineHttpClient::class);
|
||||||
} else {
|
} elseif ('zimbra+http' === $scheme || 'zimbra+https' === $scheme) {
|
||||||
|
$connector = self::ZIMBRA_CONNECTOR;
|
||||||
|
foreach (self::MS_GRAPH_SERVICES_TO_REMOVE as $serviceId) {
|
||||||
|
$container->removeDefinition($serviceId);
|
||||||
|
}
|
||||||
|
} elseif ('null' === $scheme) {
|
||||||
$connector = NullRemoteCalendarConnector::class;
|
$connector = NullRemoteCalendarConnector::class;
|
||||||
// remove services which cannot be loaded
|
foreach (self::MS_GRAPH_SERVICES_TO_REMOVE as $serviceId) {
|
||||||
$container->removeDefinition(MapAndSubscribeUserCalendarCommand::class);
|
$container->removeDefinition($serviceId);
|
||||||
$container->removeDefinition(AzureGrantAdminConsentAndAcquireToken::class);
|
}
|
||||||
$container->removeDefinition(RemoteCalendarConnectAzureController::class);
|
} else {
|
||||||
$container->removeDefinition(MachineTokenStorage::class);
|
throw new \Symfony\Component\DependencyInjection\Exception\InvalidArgumentException('Unsupported remote calendar scheme: '.$scheme);
|
||||||
$container->removeDefinition(MachineHttpClient::class);
|
|
||||||
$container->removeDefinition(MSGraphRemoteCalendarConnector::class);
|
|
||||||
$container->removeDefinition(MSUserAbsenceReaderInterface::class);
|
|
||||||
$container->removeDefinition(MSUserAbsenceSync::class);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!$container->hasAlias(Azure::class) && $container->hasDefinition('knpu.oauth2.client.azure')) {
|
if (!$container->hasAlias(Azure::class) && $container->hasDefinition('knpu.oauth2.client.azure')) {
|
||||||
@@ -62,7 +83,9 @@ class RemoteCalendarCompilerPass implements CompilerPassInterface
|
|||||||
|
|
||||||
foreach ([
|
foreach ([
|
||||||
NullRemoteCalendarConnector::class,
|
NullRemoteCalendarConnector::class,
|
||||||
MSGraphRemoteCalendarConnector::class, ] as $serviceId) {
|
MSGraphRemoteCalendarConnector::class,
|
||||||
|
self::ZIMBRA_CONNECTOR,
|
||||||
|
] as $serviceId) {
|
||||||
if ($connector === $serviceId) {
|
if ($connector === $serviceId) {
|
||||||
$container->getDefinition($serviceId)
|
$container->getDefinition($serviceId)
|
||||||
->setDecoratedService(RemoteCalendarConnectorInterface::class);
|
->setDecoratedService(RemoteCalendarConnectorInterface::class);
|
||||||
|
|||||||
@@ -87,6 +87,8 @@ remote_ms_graph:
|
|||||||
|
|
||||||
remote_calendar:
|
remote_calendar:
|
||||||
calendar_range_title: Plage de disponibilité Chill
|
calendar_range_title: Plage de disponibilité Chill
|
||||||
|
# small type-hint in remote calendar to says that the appointment is created through an invitation, and not as main referrer
|
||||||
|
calendar_invite_statement_in_calendar: Par invitation
|
||||||
|
|
||||||
invite:
|
invite:
|
||||||
accepted: Accepté
|
accepted: Accepté
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ class AddressRender implements ChillEntityRenderInterface
|
|||||||
'with_delimiter' => false,
|
'with_delimiter' => false,
|
||||||
'has_no_address' => false,
|
'has_no_address' => false,
|
||||||
'multiline' => true,
|
'multiline' => true,
|
||||||
|
'separator' => ' — ',
|
||||||
/* deprecated */
|
/* deprecated */
|
||||||
'extended_infos' => false,
|
'extended_infos' => false,
|
||||||
];
|
];
|
||||||
@@ -114,7 +115,9 @@ class AddressRender implements ChillEntityRenderInterface
|
|||||||
|
|
||||||
public function renderString($addr, array $options): string
|
public function renderString($addr, array $options): string
|
||||||
{
|
{
|
||||||
return implode(' — ', $this->renderLines($addr));
|
$opts = [...self::DEFAULT_OPTIONS, ...$options];
|
||||||
|
|
||||||
|
return implode($opts['separator'], $this->renderLines($addr));
|
||||||
}
|
}
|
||||||
|
|
||||||
public function supports($entity, array $options): bool
|
public function supports($entity, array $options): bool
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ class CommentRender implements ChillEntityRenderInterface
|
|||||||
|
|
||||||
public function renderString($entity, array $options): string
|
public function renderString($entity, array $options): string
|
||||||
{
|
{
|
||||||
return $entity->getComment();
|
return (string) $entity->getComment();
|
||||||
}
|
}
|
||||||
|
|
||||||
public function supports($entity, array $options): bool
|
public function supports($entity, array $options): bool
|
||||||
|
|||||||
@@ -2,6 +2,9 @@
|
|||||||
"champs-libres/wopi-bundle": {
|
"champs-libres/wopi-bundle": {
|
||||||
"version": "dev-master"
|
"version": "dev-master"
|
||||||
},
|
},
|
||||||
|
"chill-project/chill-zimbra-bundle": {
|
||||||
|
"version": "dev-472-zimbra-connector"
|
||||||
|
},
|
||||||
"doctrine/annotations": {
|
"doctrine/annotations": {
|
||||||
"version": "1.14",
|
"version": "1.14",
|
||||||
"recipe": {
|
"recipe": {
|
||||||
|
|||||||
Reference in New Issue
Block a user