add events and command to retrieve them using caldav calendars
This commit is contained in:
parent
4d013a4550
commit
3d8cacd62c
|
@ -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
|
||||
|
|
11
README.md
11
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
|
||||
```
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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'],
|
||||
|
|
|
@ -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);
|
||||
})
|
||||
;
|
||||
|
|
|
@ -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
|
||||
{
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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' => [
|
||||
],
|
||||
]
|
||||
);
|
||||
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
;
|
||||
}
|
||||
*/
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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')
|
||||
;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>
|
|
@ -0,0 +1,13 @@
|
|||
<?php
|
||||
|
||||
namespace App\Security;
|
||||
|
||||
/**
|
||||
* Interface EncryptedEntityInterface.
|
||||
*
|
||||
* @author Simon Vieille <simon@deblan.fr>
|
||||
*/
|
||||
interface EncryptedEntityInterface
|
||||
{
|
||||
public function getEncryptedProperties(): array;
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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 %}
|
||||
|
|
@ -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 %}
|
|
@ -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>
|
|
@ -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"
|
||||
|
|
Loading…
Reference in New Issue