add events and command to retrieve them using caldav calendars

This commit is contained in:
Simon Vieille 2022-04-16 23:29:17 +02:00
parent 4d013a4550
commit 3d8cacd62c
Signed by: deblan
GPG Key ID: 03383D15A1D31745
27 changed files with 1120 additions and 15 deletions

2
.gitignore vendored
View File

@ -3,7 +3,7 @@
/.env.local
/.env.local.php
/.env.*.local
/config/secrets/prod/prod.decrypt.private.php
/config/secrets/*
/public/bundles/
/public/js/
/src/Command/TestCommand.php

View File

@ -14,3 +14,14 @@ Murph is an **open-source CMF** built on top of Symfony that helps you to **buil
**End users will be fond of the interface and the powerful tools 💜**
📗 [Read the documentation](https://doc.murph-project.org/)
## Suivi
### Security
```
$ mkdir config/secrets
$ openssl genrsa -out config/secrets/prod.private.key 4096
$ openssl rsa -in config/secrets/prod.private.key -out config/secrets/prod.public.key -pubout -outform PEM
```

View File

@ -7,7 +7,8 @@
"prefer-stable": true,
"require": {
"php": ">=8.0.0",
"murph/murph-core": "dev-master"
"murph/murph-core": "dev-master",
"sabre/dav": "^4.3"
},
"require-dev": {
"symfony/browser-kit": "^5.4",

View File

@ -58,8 +58,12 @@ services:
calls:
- [ setAnnotationReader, [ "@annotation_reader" ] ]
App\UrlGenerator\FooUrlGenerator:
App\Security\OpenSSL:
public: true
arguments:
$publicKeyPath: '%kernel.project_dir%/config/secrets/prod.public.key'
$privateKeyPath: '%kernel.project_dir%/config/secrets/prod.private.key'
$filesystem: '@filesystem'
# add more service definitions when explicit configuration is needed
# please note that last definitions always *replace* previous ones

View File

@ -0,0 +1,119 @@
<?php
namespace App\Command;
use App\Entity\Project;
use App\Repository\ProjectRepositoryQuery;
use App\Repository\SpeakerRepositoryQuery;
use App\Security\OpenSSL;
use App\Webdav\CaldavClient;
use App\Webdav\Event as CalEvent;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Console\Style\SymfonyStyle;
use App\Core\Manager\EntityManager;
use App\Repository\EventRepositoryQuery;
use App\Factory\EventFactory;
use App\Entity\Speaker;
#[AsCommand(
name: 'caldav:sync',
description: 'Sync caldav events',
)]
class CaldavSyncCommand extends Command
{
protected SpeakerRepositoryQuery $speakerQuery;
protected ProjectRepositoryQuery $projectQuery;
protected EventRepositoryQuery $eventQuery;
protected EventFactory $eventFactory;
protected OpenSSL $openSSL;
public function __construct(
SpeakerRepositoryQuery $speakerQuery,
ProjectRepositoryQuery $projectQuery,
EventRepositoryQuery $eventQuery,
EventFactory $eventFactory,
EntityManager $manager,
OpenSSL $openSSL
) {
parent::__construct();
$this->speakerQuery = $speakerQuery;
$this->projectQuery = $projectQuery;
$this->eventQuery = $eventQuery;
$this->eventFactory = $eventFactory;
$this->manager = $manager;
$this->openSSL = $openSSL;
}
protected function getSpeakers(): array
{
$speakers = $this->speakerQuery->withCalendar()->find();
foreach ($speakers as $speaker) {
$this->openSSL->decryptEntity($speaker);
}
return $speakers;
}
protected function getProjects(): array
{
return $this->projectQuery->find();
}
protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);
$speakers = $this->getSpeakers();
$projects = $this->getProjects();
foreach ($speakers as $speaker) {
$client = new CaldavClient(
$speaker->getCaldavHost(),
$speaker->getCaldavUsername(),
$speaker->getCaldavPassword()
);
$this->openSSL->encryptEntity($speaker);
$calEvents = $client->getEvents($speaker->getCaldavCalendarUri());
foreach ($projects as $project) {
foreach ($calEvents as $calEvent) {
$regex = sprintf('/%s/', preg_quote($project->getCaldavEventTag()));
if (!preg_match($regex, $calEvent->getDescription())) {
continue;
}
$this->addEvent($project, $calEvent, $speaker);
}
}
}
return Command::SUCCESS;
}
protected function addEvent(Project $project, CalEvent $calEvent, Speaker $speaker)
{
$event = $this->eventQuery->create()
->where('.uid = :uid')
->setParameter('uid', $calEvent->getUid())
->findOne();
if (!$event) {
$event = $this->eventFactory->createFromEvent($calEvent);
} else {
$this->eventFactory->updateFromEvent($event, $calEvent);
}
$project->addEvent($event);
$speaker->addEvent($event);
$this->manager->update($event);
$this->manager->update($speaker);
}
}

View File

@ -109,17 +109,21 @@ class ProjectAdminController extends CrudController
// ->setSortableCollectionProperty('sortOrder')
// ->setAction('index', 'new', true)
->setAction('index', 'show', false)
// ->setAction('index', 'show', false)
// ->setAction('index', 'edit', true)
// ->setAction('index', 'delete', true)
// ->setAction('edit', 'back', true)
->setAction('edit', 'show', false)
// ->setAction('edit', 'show', false)
// ->setAction('edit', 'delete', true)
// ->setAction('show', 'back', true)
// ->setAction('show', 'edit', true)
->setView('show', 'admin/project/show.html.twig')
->setView('show_entity', 'admin/project/_show.html.twig')
->setView('edit', 'admin/project/edit.html.twig')
->setField('index', 'Label', Field\TextField::class, [
'property' => 'label',
'sort' => ['label', '.label'],

View File

@ -11,6 +11,7 @@ use App\Entity\Speaker as Entity;
use App\Factory\SpeakerFactory as Factory;
use App\Form\SpeakerType as Type;
use App\Repository\SpeakerRepositoryQuery as RepositoryQuery;
use App\Security\OpenSSL;
use Symfony\Component\HttpFoundation\Request;
use Symfony\Component\HttpFoundation\Response;
use Symfony\Component\HttpFoundation\Session\Session;
@ -29,9 +30,11 @@ class SpeakerAdminController extends CrudController
/**
* @Route("/admin/speaker/new", name="admin_speaker_new", methods={"GET", "POST"})
*/
public function new(Factory $factory, EntityManager $entityManager, Request $request): Response
public function new(Factory $factory, EntityManager $entityManager, Request $request, OpenSSL $openSSL): Response
{
return $this->doNew($factory->create(), $entityManager, $request);
return $this->doNew($factory->create(), $entityManager, $request, function (Entity $entity) use ($openSSL) {
$openSSL->encryptEntity($entity);
});
}
/**
@ -53,9 +56,18 @@ class SpeakerAdminController extends CrudController
/**
* @Route("/admin/speaker/edit/{entity}", name="admin_speaker_edit", methods={"GET", "POST"})
*/
public function edit(Entity $entity, EntityManager $entityManager, Request $request): Response
public function edit(Entity $entity, EntityManager $entityManager, Request $request, OpenSSL $openSSL): Response
{
return $this->doEdit($entity, $entityManager, $request);
$openSSL->decryptEntity($entity);
$caldavPassword = $entity->getCaldavPassword();
return $this->doEdit($entity, $entityManager, $request, function (Entity $entity) use ($openSSL, $caldavPassword) {
if (!$entity->getCaldavPassword()) {
$entity->setCaldavPassword($caldavPassword);
}
$openSSL->encryptEntity($entity);
});
}
/**
@ -122,12 +134,19 @@ class SpeakerAdminController extends CrudController
->setDefaultSort('index', 'name')
->setFormOptions('new', [
'edit_caldav' => $this->isGranted('ROLE_ADMIN'),
])
->setFormOptions('edit', [
'edit_caldav' => $this->isGranted('ROLE_ADMIN'),
])
->setField('index', 'Name', Field\TextField::class, [
'property' => 'name',
'sort' => ['name', '.name'],
])
->setBatchAction('index', 'delete', 'Delete', function(EntityInterface $entity, EntityManager $manager) {
->setBatchAction('index', 'delete', 'Delete', function (EntityInterface $entity, EntityManager $manager) {
$manager->delete($entity);
})
;

View File

@ -29,7 +29,7 @@ class Conference implements EntityInterface
protected $persons;
/**
* @ORM\ManyToOne(targetEntity=ThemeType::class, inversedBy="conferences")
* @ORM\ManyToOne(targetEntity=ThemeType::class, inversedBy="conferences", cascade={"persist", "remove"})
* @ORM\JoinColumn(nullable=false)
*/
protected $themeType;
@ -52,7 +52,7 @@ class Conference implements EntityInterface
/**
* @ORM\Column(type="date", nullable=true)
*/
private $date;
protected $date;
public function getId(): ?int
{

169
src/Entity/Event.php Normal file
View File

@ -0,0 +1,169 @@
<?php
namespace App\Entity;
use App\Core\Entity\EntityInterface;
use App\Repository\EventRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
/**
* @ORM\Entity(repositoryClass=EventRepository::class)
*/
class Event implements EntityInterface
{
/**
* @ORM\Id
* @ORM\GeneratedValue
* @ORM\Column(type="integer")
*/
protected $id;
/**
* @ORM\Column(type="string", length=40)
*/
protected $uid;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
protected $summary;
/**
* @ORM\Column(type="text", nullable=true)
*/
protected $description;
/**
* @ORM\Column(type="datetime", nullable=true)
*/
protected $startAt;
/**
* @ORM\Column(type="datetime", nullable=true)
*/
protected $finishAt;
/**
* @ORM\ManyToOne(targetEntity=Project::class, inversedBy="events", cascade={"persist", "remove"})
* @ORM\JoinColumn(nullable=false)
*/
protected $projects;
/**
* @ORM\ManyToMany(targetEntity=Speaker::class, inversedBy="events")
*/
private $speakers;
public function __construct()
{
$this->speakers = new ArrayCollection();
}
public function getId(): ?int
{
return $this->id;
}
public function getUid(): ?string
{
return $this->uid;
}
public function setUid(string $uid): self
{
$this->uid = $uid;
return $this;
}
public function getSummary(): ?string
{
return $this->summary;
}
public function setSummary(?string $summary): self
{
$this->summary = $summary;
return $this;
}
public function getCleanedDescription(): string
{
return trim(preg_replace('/\s*#\{project:\d+\}\s*/', "\n", $this->getDescription()));
}
public function getDescription(): ?string
{
return $this->description;
}
public function setDescription(?string $description): self
{
$this->description = $description;
return $this;
}
public function getStartAt(): ?\DateTimeInterface
{
return $this->startAt;
}
public function setStartAt(?\DateTimeInterface $startAt): self
{
$this->startAt = $startAt;
return $this;
}
public function getFinishAt(): ?\DateTimeInterface
{
return $this->finishAt;
}
public function setFinishAt(?\DateTimeInterface $finishAt): self
{
$this->finishAt = $finishAt;
return $this;
}
public function getProjects(): ?Project
{
return $this->projects;
}
public function setProjects(?Project $projects): self
{
$this->projects = $projects;
return $this;
}
/**
* @return Collection<int, Speaker>
*/
public function getSpeakers(): Collection
{
return $this->speakers;
}
public function addSpeaker(Speaker $speaker): self
{
if (!$this->speakers->contains($speaker)) {
$this->speakers[] = $speaker;
}
return $this;
}
public function removeSpeaker(Speaker $speaker): self
{
$this->speakers->removeElement($speaker);
return $this;
}
}

View File

@ -50,10 +50,16 @@ class Project implements EntityInterface
*/
protected $debriefings;
/**
* @ORM\OneToMany(targetEntity=Event::class, mappedBy="projects", cascade={"persist", "remove"})
*/
protected $events;
public function __construct()
{
$this->establishments = new ArrayCollection();
$this->debriefings = new ArrayCollection();
$this->events = new ArrayCollection();
}
public function __toString()
@ -128,7 +134,7 @@ class Project implements EntityInterface
public function getFiles(): ?array
{
usort($this->files, function($a, $b) {
usort($this->files, function ($a, $b) {
return $a['position'] <=> $b['position'];
});
@ -168,4 +174,45 @@ class Project implements EntityInterface
return $this;
}
public function getCaldavEventTag(): string
{
return sprintf('#{project:%d}', $this->getId());
}
/**
* @return Collection<int, Event>
*/
public function getEvents(): Collection
{
$events = $this->events->toArray();
usort($events, function($a, $b) {
return $a->getStartAt() <=> $a->getFinishAt();
});
return new ArrayCollection($events);
}
public function addEvent(Event $event): self
{
if (!$this->events->contains($event)) {
$this->events[] = $event;
$event->setProjects($this);
}
return $this;
}
public function removeEvent(Event $event): self
{
if ($this->events->removeElement($event)) {
// set the owning side to null (unless already changed)
if ($event->getProjects() === $this) {
$event->setProjects(null);
}
}
return $this;
}
}

View File

@ -7,11 +7,12 @@ use App\Repository\SpeakerRepository;
use Doctrine\Common\Collections\ArrayCollection;
use Doctrine\Common\Collections\Collection;
use Doctrine\ORM\Mapping as ORM;
use App\Security\EncryptedEntityInterface;
/**
* @ORM\Entity(repositoryClass=SpeakerRepository::class)
*/
class Speaker implements EntityInterface
class Speaker implements EntityInterface, EncryptedEntityInterface
{
/**
* @ORM\Id
@ -30,9 +31,35 @@ class Speaker implements EntityInterface
*/
protected $interventions;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
protected $caldavHost;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
protected $caldavUsername;
/**
* @ORM\Column(type="blob", nullable=true)
*/
protected $caldavPassword;
/**
* @ORM\Column(type="string", length=255, nullable=true)
*/
protected $caldavCalendarUri;
/**
* @ORM\ManyToMany(targetEntity=Event::class, mappedBy="speakers")
*/
private $events;
public function __construct()
{
$this->interventions = new ArrayCollection();
$this->events = new ArrayCollection();
}
public function __toString()
@ -83,4 +110,86 @@ class Speaker implements EntityInterface
return $this;
}
public function getCaldavHost(): ?string
{
return $this->caldavHost;
}
public function setCaldavHost(?string $caldavHost): self
{
$this->caldavHost = $caldavHost;
return $this;
}
public function getCaldavUsername(): ?string
{
return $this->caldavUsername;
}
public function setCaldavUsername(?string $caldavUsername): self
{
$this->caldavUsername = $caldavUsername;
return $this;
}
public function getCaldavPassword()
{
return $this->caldavPassword;
}
public function setCaldavPassword(?string $caldavPassword): self
{
$this->caldavPassword = $caldavPassword;
return $this;
}
public function getCaldavCalendarUri(): ?string
{
return $this->caldavCalendarUri;
}
public function setCaldavCalendarUri(?string $caldavCalendarUri): self
{
$this->caldavCalendarUri = $caldavCalendarUri;
return $this;
}
public function getEncryptedProperties(): array
{
return [
'caldavPassword',
];
}
/**
* @return Collection<int, Event>
*/
public function getEvents(): Collection
{
return $this->events;
}
public function addEvent(Event $event): self
{
if (!$this->events->contains($event)) {
$this->events[] = $event;
$event->addSpeaker($this);
}
return $this;
}
public function removeEvent(Event $event): self
{
if ($this->events->removeElement($event)) {
$event->removeSpeaker($this);
}
return $this;
}
}

View File

@ -0,0 +1,42 @@
<?php
namespace App\Factory;
use App\Core\Factory\FactoryInterface;
use App\Entity\Event as Entity;
use App\Webdav\Event as CalEvent;
class EventFactory implements FactoryInterface
{
public function create(): Entity
{
return new Entity();
}
public function createFromEvent(CalEvent $event): Entity
{
$entity = new Entity();
$entity
->setUid($event->getUid())
->setSummary($event->getSummary())
->setDescription($event->getDescription())
->setStartAt($event->getStartAt())
->setFinishAt($event->getFinishAt())
;
return $entity;
}
public function updateFromEvent(Entity $entity, CalEvent $event): Entity
{
$entity
->setUid($event->getUid())
->setSummary($event->getSummary())
->setDescription($event->getDescription())
->setStartAt($event->getStartAt())
->setFinishAt($event->getFinishAt())
;
return $entity;
}
}

View File

@ -6,6 +6,7 @@ use App\Entity\Speaker;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Form\Extension\Core\Type\PasswordType;
class SpeakerType extends AbstractType
{
@ -14,12 +15,28 @@ class SpeakerType extends AbstractType
$builder
->add('name')
;
if ($options['edit_caldav']) {
$builder
->add('caldavHost', null, [
'help' => 'https://exemple.fr/remote.php/dav'
])
->add('caldavUsername')
->add('caldavPassword', PasswordType::class, [
'required' => false,
])
->add('caldavCalendarUri', null, [
'help' => '/remote.php/dav/calendars/johnDoe/myCalendar/'
])
;
}
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'data_class' => Speaker::class,
'edit_caldav' => false,
]);
}
}

View File

@ -3,7 +3,27 @@
namespace App\Form;
use App\Core\Form\UserType as BaseUserType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\Form\Extension\Core\Type\CheckboxType;
class UserType extends BaseUserType
{
public function buildForm(FormBuilderInterface $builder, array $options)
{
parent::buildForm($builder, $options);
$builder->add(
'isManager',
CheckboxType::class,
[
'label' => 'Coordinateur⋅trice',
'required' => false,
'attr' => [
],
'constraints' => [
],
]
);
}
}

View File

@ -0,0 +1,76 @@
<?php
namespace App\Repository;
use App\Entity\Event;
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
use Doctrine\ORM\OptimisticLockException;
use Doctrine\ORM\ORMException;
use Doctrine\Persistence\ManagerRegistry;
/**
* @method Event|null find($id, $lockMode = null, $lockVersion = null)
* @method Event|null findOneBy(array $criteria, array $orderBy = null)
* @method Event[] findAll()
* @method Event[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
*/
class EventRepository extends ServiceEntityRepository
{
public function __construct(ManagerRegistry $registry)
{
parent::__construct($registry, Event::class);
}
/**
* @throws ORMException
* @throws OptimisticLockException
*/
public function add(Event $entity, bool $flush = true): void
{
$this->_em->persist($entity);
if ($flush) {
$this->_em->flush();
}
}
/**
* @throws ORMException
* @throws OptimisticLockException
*/
public function remove(Event $entity, bool $flush = true): void
{
$this->_em->remove($entity);
if ($flush) {
$this->_em->flush();
}
}
// /**
// * @return Event[] Returns an array of Event objects
// */
/*
public function findByExampleField($value)
{
return $this->createQueryBuilder('e')
->andWhere('e.exampleField = :val')
->setParameter('val', $value)
->orderBy('e.id', 'ASC')
->setMaxResults(10)
->getQuery()
->getResult()
;
}
*/
/*
public function findOneBySomeField($value): ?Event
{
return $this->createQueryBuilder('e')
->andWhere('e.exampleField = :val')
->setParameter('val', $value)
->getQuery()
->getOneOrNullResult()
;
}
*/
}

View File

@ -0,0 +1,15 @@
<?php
namespace App\Repository;
use App\Core\Repository\RepositoryQuery;
use Knp\Component\Pager\PaginatorInterface;
use App\Repository\EventRepository as Repository;
class EventRepositoryQuery extends RepositoryQuery
{
public function __construct(Repository $repository, PaginatorInterface $paginator)
{
parent::__construct($repository, 'e', $paginator);
}
}

View File

@ -3,8 +3,8 @@
namespace App\Repository;
use App\Core\Repository\RepositoryQuery;
use Knp\Component\Pager\PaginatorInterface;
use App\Repository\SpeakerRepository as Repository;
use Knp\Component\Pager\PaginatorInterface;
class SpeakerRepositoryQuery extends RepositoryQuery
{
@ -12,4 +12,14 @@ class SpeakerRepositoryQuery extends RepositoryQuery
{
parent::__construct($repository, 's', $paginator);
}
public function withCalendar()
{
return $this
->andWhere('.caldavHost IS NOT NULL')
->andWhere('.caldavUsername IS NOT NULL')
->andWhere('.caldavPassword IS NOT NULL')
->andWhere('.caldavCalendarUri IS NOT NULL')
;
}
}

View File

@ -0,0 +1,17 @@
<?xml version="1.0" ?>
<container xmlns="http://symfony.com/schema/dic/services"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://symfony.com/schema/dic/services http://symfony.com/schema/dic/services/services-1.0.xsd">
<parameters>
<parameter key="app.security.openssl.class">Zen\Bundle\AppBundle\Security\OpenSSL</parameter>
</parameters>
<service id="app.security.openssl" class="%app.security.openssl.class%" public="true">
<argument>file://%kernel.project_dir%/app/ssl/public.key</argument>
<argument>file://%kernel.project_dir%/app/ssl/private.key</argument>
<argument type="service" id="filesystem" />
</service>
<service id="%app.security.openssl.class%" alias="app.security.openssl" public="true"></service>
</services>
</container>

View File

@ -0,0 +1,13 @@
<?php
namespace App\Security;
/**
* Interface EncryptedEntityInterface.
*
* @author Simon Vieille <simon@deblan.fr>
*/
interface EncryptedEntityInterface
{
public function getEncryptedProperties(): array;
}

86
src/Security/OpenSSL.php Normal file
View File

@ -0,0 +1,86 @@
<?php
namespace App\Security;
use Symfony\Component\Filesystem\Filesystem;
/**
* class OpenSSL.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class OpenSSL
{
protected ?\OpenSSLAsymmetricKey $publicKey;
protected ?\OpenSSLAsymmetricKey $privateKey;
public function __construct(string $publicKeyPath, string $privateKeyPath, Filesystem $filesystem)
{
if ($filesystem->exists($publicKeyPath)) {
$this->publicKey = openssl_pkey_get_public(file_get_contents($publicKeyPath));
}
if ($filesystem->exists($privateKeyPath)) {
$this->privateKey = openssl_pkey_get_private(file_get_contents($privateKeyPath));
}
}
/**
* Encrypts data by using the public key.
*/
public function encrypt($data): ?string
{
if (empty($this->publicKey)) {
throw new \RuntimeException('Public key needed.');
}
openssl_public_encrypt($data, $result, $this->publicKey);
return $result;
}
/**
* Decrypts data by using the private key.
*/
public function decrypt($data): ?string
{
if (empty($this->privateKey)) {
throw new \RuntimeException('Private key needed.');
}
openssl_private_decrypt($data, $result, $this->privateKey);
return $result;
}
public function decryptEntity(EncryptedEntityInterface $entity): EncryptedEntityInterface
{
foreach ($entity->getEncryptedProperties() as $property) {
$getter = 'get'.$property;
$setter = 'set'.$property;
$encryptedValue = $entity->{$getter}();
if (!is_resource($encryptedValue)) {
continue;
}
$encryptedValue = stream_get_contents($encryptedValue, -1, 0);
$entity->{$setter}($this->decrypt($encryptedValue));
}
return $entity;
}
public function encryptEntity(EncryptedEntityInterface $entity): EncryptedEntityInterface
{
foreach ($entity->getEncryptedProperties() as $property) {
$getter = 'get'.$property;
$setter = 'set'.$property;
$value = (string) $entity->{$getter}();
$entity->{$setter}($this->encrypt($value));
}
return $entity;
}
}

123
src/Webdav/CaldavClient.php Normal file
View File

@ -0,0 +1,123 @@
<?php
namespace App\Webdav;
use Sabre\DAV\Client;
/**
* class CaldavClient.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class CaldavClient
{
protected string $host;
protected string $username;
protected string $password;
protected ?Client $client = null;
public function __construct(string $host, string $username, string $password)
{
$this->host = $host;
$this->username = $username;
$this->password = $password;
}
protected function getClient(): Client
{
if (null === $this->client) {
$this->client = new Client([
'baseUri' => $this->host,
'userName' => $this->username,
'password' => $this->password,
]);
}
return $this->client;
}
public function getEvents(string $calendarUri)
{
$body = '<c:calendar-query xmlns:d="DAV:" xmlns:c="urn:ietf:params:xml:ns:caldav">
<d:prop>
<d:getetag />
<c:calendar-data />
</d:prop>
<c:filter>
<c:comp-filter name="VCALENDAR">
</c:comp-filter>
</c:filter>
</c:calendar-query>';
$response = $this->getClient()->request(
'REPORT',
$calendarUri,
$body,
[
'Depth' => 1,
'Prefer' => 'return-minimal',
'Content-Type' => 'application/xml; charset=utf-8',
]
);
$body = $response['body'];
$events = [];
preg_match_all('#<cal:calendar-data>(.+)</cal:calendar-data>#isU', $body, $rawEvents, PREG_SET_ORDER);
foreach ($rawEvents as $rawEvent) {
$events[] = $this->createEvent($rawEvent[1]);
}
return $events;
}
protected function createEvent(string $rawEvent): Event
{
preg_match_all('/
^([A-Z\-]+)[:;]{1}([^\n]+)\n
((\ [^\n]+\n)*\3*)
/xm', $rawEvent, $matches, PREG_SET_ORDER);
$datas = [];
foreach ($matches as $match) {
$key = $match[1];
if (isset($datas[$key])) {
continue;
}
$lines = array_map(fn ($line) => trim($line), explode("\n", $match[2].$match[3]));
$lines = str_replace('\\n', "\n", implode('', $lines));
if (in_array($key, ['DTSTART', 'DTEND'])) {
if (empty($lines)) {
continue;
}
@list($tz, $time) = explode(':', $lines);
if (empty($time)) {
continue;
}
$lines = new \DateTime($time);
}
$datas[$key] = $lines;
}
$event = new Event();
$event
->setUid($datas['UID'])
->setSummary($datas['SUMMARY'] ?? null)
->setDescription($datas['DESCRIPTION'] ?? null)
->setLocation($datas['LOCATION'] ?? null)
->setSummary($datas['SUMMARY'] ?? null)
->setStartAt($datas['DTSTART'] ?? null)
->setFinishAt($datas['DTEND'] ?? null)
;
return $event;
}
}

90
src/Webdav/Event.php Normal file
View File

@ -0,0 +1,90 @@
<?php
namespace App\Webdav;
/**
* class Event.
*
* @author Simon Vieille <simon@deblan.fr>
*/
class Event
{
protected ?string $uid;
protected ?string $summary;
protected ?string $description;
protected ?string $location;
protected ?\DateTime $startAt;
protected ?\DateTime $finishAt;
public function setUid(?string $uid): self
{
$this->uid = $uid;
return $this;
}
public function getUid(): ?string
{
return $this->uid;
}
public function setSummary(?string $summary): self
{
$this->summary = $summary;
return $this;
}
public function getSummary(): ?string
{
return $this->summary;
}
public function setDescription(?string $description): self
{
$this->description = $description;
return $this;
}
public function getDescription(): ?string
{
return $this->description;
}
public function setLocation(?string $location): self
{
$this->location = $location;
return $this;
}
public function getLocation(): ?string
{
return $this->location;
}
public function setStartAt(?\DateTime $startAt): self
{
$this->startAt = $startAt;
return $this;
}
public function getStartAt(): ?\DateTime
{
return $this->startAt;
}
public function setFinishAt(?\DateTime $finishAt): self
{
$this->finishAt = $finishAt;
return $this;
}
public function getFinishAt(): ?\DateTime
{
return $this->finishAt;
}
}

View File

@ -0,0 +1,79 @@
<div class="row">
<div class="col-md-3 p-3">
<ul class="list-group">
<li class="list-group-item">
<span class="font-weight-bold pb-2 d-block">Établissements</span>
{% for item in entity.establishments %}
{{ item.name }}<br>
{% else %}
-
{% endfor %}
</li>
<li class="list-group-item">
<span class="font-weight-bold pb-2 d-block">Client</span>
{{ entity.client|nl2br }}
</li>
<li class="list-group-item">
<span class="font-weight-bold pb-2 d-block">Fichiers</span>
{% for item in entity.files %}
<a class="btn btn-light border btn-sm mb-2 d-block" href="{{ asset(item.file) }}">
{{ item.name }}
</a>
{% else %}
-
{% endfor %}
</li>
</ul>
</div>
<div class="col-md-9 p-3">
<p class="font-weight-bold">{{ entity.label }}</p>
{{ entity.description|raw }}
<div class="pt-4">
<div class="row">
{% for event in entity.events %}
<div class="col-12 card mb-2">
<div class="card-body">
<div class="h5 card-title">{{ event.summary }}</div>
<div class="card-text">
{% set description = event.cleanedDescription %}
<div class="{{ description ? 'mb-2' : '' }}">
{% if event.startAt or event.finishAt %}
{% if event.startAt %}
<span class="btn btn-sm btn-light mr-1">
<span class="fa fa-calendar-alt text-black-50 mr-2"></span>
{{ event.startAt|date('d/m/Y H:i') }}
</span>
{% endif %}
{% if event.finishAt %}
<span class="btn btn-sm btn-light mr-1">
<span class="fa fa-calendar-alt text-black-50 mr-2"></span>
{{ event.finishAt|date('d/m/Y H:i') }}
</span>
{% endif %}
{% endif %}
{% for speaker in event.speakers %}
<span class="btn btn-primary btn-sm mr-1">
{{ speaker.name }}
</span>
{% endfor %}
</div>
{{ description|nl2br }}
</div>
</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>

View File

@ -0,0 +1,10 @@
{% extends '@Core/admin/crud/edit.html.twig' %}
{% block header %}
{{ parent() }}
<div class="alert alert-info">
Tag à ajouter dans les évènements du calendrier : <strong>{{ entity.caldavEventTag }}</strong>
</div>
{% endblock %}

View File

@ -0,0 +1,9 @@
{% extends '@Core/admin/crud/show.html.twig' %}
{% block header %}
{{ parent() }}
<div class="alert alert-info">
Tag à ajouter dans les évènements du calendrier : <strong>{{ entity.caldavEventTag }}</strong>
</div>
{% endblock %}

View File

@ -0,0 +1,11 @@
<div class="row">
<div class="col-12 p-3">
<div class="row">
{% for item in ['displayName', 'email', 'isAdmin', 'isWriter', 'isManager'] %}
<div class="col-12">
{{ form_row(form[item]) }}
</div>
{% endfor %}
</div>
</div>
</div>

View File

@ -21,3 +21,7 @@
"Project": "Projet"
"Internal contributors": "Contributeurs internes"
"External contributors": "Contributeurs externes"
"Caldav host": "Domaine du serveur Caldav"
"Caldav username": "Nom d'utilisateur du serveur Caldav"
"Caldav password": "Mot de passe du serveur Caldav"
"Caldav calendar uri": "URI du calendrier Caldav"