From 3d8cacd62c2e28a3b815200205bcdca0b4017324 Mon Sep 17 00:00:00 2001 From: Simon Vieille Date: Sat, 16 Apr 2022 23:29:17 +0200 Subject: [PATCH] add events and command to retrieve them using caldav calendars --- .gitignore | 2 +- README.md | 11 ++ composer.json | 3 +- config/services.yaml | 6 +- src/Command/CaldavSyncCommand.php | 119 ++++++++++++ src/Controller/ProjectAdminController.php | 8 +- src/Controller/SpeakerAdminController.php | 29 ++- src/Entity/Conference.php | 4 +- src/Entity/Event.php | 169 ++++++++++++++++++ src/Entity/Project.php | 49 ++++- src/Entity/Speaker.php | 111 +++++++++++- src/Factory/EventFactory.php | 42 +++++ src/Form/SpeakerType.php | 17 ++ src/Form/UserType.php | 20 +++ src/Repository/EventRepository.php | 76 ++++++++ src/Repository/EventRepositoryQuery.php | 15 ++ src/Repository/SpeakerRepositoryQuery.php | 12 +- src/Resources/services.xml | 17 ++ src/Security/EncryptedEntityInterface.php | 13 ++ src/Security/OpenSSL.php | 86 +++++++++ src/Webdav/CaldavClient.php | 123 +++++++++++++ src/Webdav/Event.php | 90 ++++++++++ templates/admin/project/_show.html.twig | 79 ++++++++ templates/admin/project/edit.html.twig | 10 ++ templates/admin/project/show.html.twig | 9 + .../core/user/user_admin/_form.html.twig | 11 ++ translations/messages.fr.yaml | 4 + 27 files changed, 1120 insertions(+), 15 deletions(-) create mode 100644 src/Command/CaldavSyncCommand.php create mode 100644 src/Entity/Event.php create mode 100644 src/Factory/EventFactory.php create mode 100644 src/Repository/EventRepository.php create mode 100644 src/Repository/EventRepositoryQuery.php create mode 100644 src/Resources/services.xml create mode 100644 src/Security/EncryptedEntityInterface.php create mode 100644 src/Security/OpenSSL.php create mode 100644 src/Webdav/CaldavClient.php create mode 100644 src/Webdav/Event.php create mode 100644 templates/admin/project/_show.html.twig create mode 100644 templates/admin/project/edit.html.twig create mode 100644 templates/admin/project/show.html.twig create mode 100644 templates/core/user/user_admin/_form.html.twig diff --git a/.gitignore b/.gitignore index c2d6405..eac073f 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/README.md b/README.md index 928d935..35a18f3 100644 --- a/README.md +++ b/README.md @@ -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 +``` diff --git a/composer.json b/composer.json index 7ce2c7d..c1b8970 100644 --- a/composer.json +++ b/composer.json @@ -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", diff --git a/config/services.yaml b/config/services.yaml index d944c1b..4db0e18 100644 --- a/config/services.yaml +++ b/config/services.yaml @@ -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 diff --git a/src/Command/CaldavSyncCommand.php b/src/Command/CaldavSyncCommand.php new file mode 100644 index 0000000..41d7058 --- /dev/null +++ b/src/Command/CaldavSyncCommand.php @@ -0,0 +1,119 @@ +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); + } +} diff --git a/src/Controller/ProjectAdminController.php b/src/Controller/ProjectAdminController.php index cc0801f..3abd95a 100644 --- a/src/Controller/ProjectAdminController.php +++ b/src/Controller/ProjectAdminController.php @@ -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'], diff --git a/src/Controller/SpeakerAdminController.php b/src/Controller/SpeakerAdminController.php index af6878d..195a874 100644 --- a/src/Controller/SpeakerAdminController.php +++ b/src/Controller/SpeakerAdminController.php @@ -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); }) ; diff --git a/src/Entity/Conference.php b/src/Entity/Conference.php index cc3a72c..9d19915 100644 --- a/src/Entity/Conference.php +++ b/src/Entity/Conference.php @@ -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 { diff --git a/src/Entity/Event.php b/src/Entity/Event.php new file mode 100644 index 0000000..00290df --- /dev/null +++ b/src/Entity/Event.php @@ -0,0 +1,169 @@ +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 + */ + 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; + } +} diff --git a/src/Entity/Project.php b/src/Entity/Project.php index 14a04c5..f4e265c 100644 --- a/src/Entity/Project.php +++ b/src/Entity/Project.php @@ -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 + */ + 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; + } } diff --git a/src/Entity/Speaker.php b/src/Entity/Speaker.php index c537ae2..3301b9e 100644 --- a/src/Entity/Speaker.php +++ b/src/Entity/Speaker.php @@ -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 + */ + 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; + } } diff --git a/src/Factory/EventFactory.php b/src/Factory/EventFactory.php new file mode 100644 index 0000000..26af0d7 --- /dev/null +++ b/src/Factory/EventFactory.php @@ -0,0 +1,42 @@ +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; + } +} diff --git a/src/Form/SpeakerType.php b/src/Form/SpeakerType.php index 4150e84..1f44901 100644 --- a/src/Form/SpeakerType.php +++ b/src/Form/SpeakerType.php @@ -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, ]); } } diff --git a/src/Form/UserType.php b/src/Form/UserType.php index 65e372a..eefa9e8 100644 --- a/src/Form/UserType.php +++ b/src/Form/UserType.php @@ -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' => [ + ], + ] + ); + + } } diff --git a/src/Repository/EventRepository.php b/src/Repository/EventRepository.php new file mode 100644 index 0000000..74ce9ab --- /dev/null +++ b/src/Repository/EventRepository.php @@ -0,0 +1,76 @@ +_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() + ; + } + */ +} diff --git a/src/Repository/EventRepositoryQuery.php b/src/Repository/EventRepositoryQuery.php new file mode 100644 index 0000000..97d7c97 --- /dev/null +++ b/src/Repository/EventRepositoryQuery.php @@ -0,0 +1,15 @@ +andWhere('.caldavHost IS NOT NULL') + ->andWhere('.caldavUsername IS NOT NULL') + ->andWhere('.caldavPassword IS NOT NULL') + ->andWhere('.caldavCalendarUri IS NOT NULL') + ; + } } diff --git a/src/Resources/services.xml b/src/Resources/services.xml new file mode 100644 index 0000000..c6e99ab --- /dev/null +++ b/src/Resources/services.xml @@ -0,0 +1,17 @@ + + + + + Zen\Bundle\AppBundle\Security\OpenSSL + + + + file://%kernel.project_dir%/app/ssl/public.key + file://%kernel.project_dir%/app/ssl/private.key + + + + + diff --git a/src/Security/EncryptedEntityInterface.php b/src/Security/EncryptedEntityInterface.php new file mode 100644 index 0000000..32ea2a7 --- /dev/null +++ b/src/Security/EncryptedEntityInterface.php @@ -0,0 +1,13 @@ + + */ +interface EncryptedEntityInterface +{ + public function getEncryptedProperties(): array; +} diff --git a/src/Security/OpenSSL.php b/src/Security/OpenSSL.php new file mode 100644 index 0000000..e51c327 --- /dev/null +++ b/src/Security/OpenSSL.php @@ -0,0 +1,86 @@ + + */ +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; + } +} diff --git a/src/Webdav/CaldavClient.php b/src/Webdav/CaldavClient.php new file mode 100644 index 0000000..8e2fa36 --- /dev/null +++ b/src/Webdav/CaldavClient.php @@ -0,0 +1,123 @@ + + */ +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 = ' + + + + + + + + + '; + + $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('#(.+)#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; + } +} diff --git a/src/Webdav/Event.php b/src/Webdav/Event.php new file mode 100644 index 0000000..2529056 --- /dev/null +++ b/src/Webdav/Event.php @@ -0,0 +1,90 @@ + + */ +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; + } +} diff --git a/templates/admin/project/_show.html.twig b/templates/admin/project/_show.html.twig new file mode 100644 index 0000000..7db5395 --- /dev/null +++ b/templates/admin/project/_show.html.twig @@ -0,0 +1,79 @@ +
+
+
    +
  • + Établissements + + {% for item in entity.establishments %} + {{ item.name }}
    + {% else %} + - + {% endfor %} +
  • +
  • + Client + + {{ entity.client|nl2br }} +
  • +
  • + Fichiers + + {% for item in entity.files %} + + {{ item.name }} + + {% else %} + - + {% endfor %} +
  • +
+
+
+

{{ entity.label }}

+ + {{ entity.description|raw }} + +
+
+ {% for event in entity.events %} +
+
+
{{ event.summary }}
+
+ {% set description = event.cleanedDescription %} + +
+ {% if event.startAt or event.finishAt %} + {% if event.startAt %} + + + + {{ event.startAt|date('d/m/Y H:i') }} + + {% endif %} + + {% if event.finishAt %} + + + + {{ event.finishAt|date('d/m/Y H:i') }} + + {% endif %} + {% endif %} + + {% for speaker in event.speakers %} + + {{ speaker.name }} + + {% endfor %} +
+ + {{ description|nl2br }} +
+
+
+ {% endfor %} +
+
+
+
diff --git a/templates/admin/project/edit.html.twig b/templates/admin/project/edit.html.twig new file mode 100644 index 0000000..fed1969 --- /dev/null +++ b/templates/admin/project/edit.html.twig @@ -0,0 +1,10 @@ +{% extends '@Core/admin/crud/edit.html.twig' %} + +{% block header %} + {{ parent() }} + +
+ Tag à ajouter dans les évènements du calendrier : {{ entity.caldavEventTag }} +
+{% endblock %} + diff --git a/templates/admin/project/show.html.twig b/templates/admin/project/show.html.twig new file mode 100644 index 0000000..ecb4062 --- /dev/null +++ b/templates/admin/project/show.html.twig @@ -0,0 +1,9 @@ +{% extends '@Core/admin/crud/show.html.twig' %} + +{% block header %} + {{ parent() }} + +
+ Tag à ajouter dans les évènements du calendrier : {{ entity.caldavEventTag }} +
+{% endblock %} diff --git a/templates/core/user/user_admin/_form.html.twig b/templates/core/user/user_admin/_form.html.twig new file mode 100644 index 0000000..3c280a5 --- /dev/null +++ b/templates/core/user/user_admin/_form.html.twig @@ -0,0 +1,11 @@ +
+
+
+ {% for item in ['displayName', 'email', 'isAdmin', 'isWriter', 'isManager'] %} +
+ {{ form_row(form[item]) }} +
+ {% endfor %} +
+
+
diff --git a/translations/messages.fr.yaml b/translations/messages.fr.yaml index b3ec7e8..ac3d267 100644 --- a/translations/messages.fr.yaml +++ b/translations/messages.fr.yaml @@ -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"