+ */
+ public function getApiTokens(): Collection
+ {
+ return $this->apiTokens;
+ }
+
+ public function addApiToken(ApiToken $apiToken): self
+ {
+ if (!$this->apiTokens->contains($apiToken)) {
+ $this->apiTokens->add($apiToken);
+ $apiToken->setUser($this);
+ }
+
+ return $this;
+ }
+
+ public function removeApiToken(ApiToken $apiToken): self
+ {
+ if ($this->apiTokens->removeElement($apiToken)) {
+ // set the owning side to null (unless already changed)
+ if ($apiToken->getUser() === $this) {
+ $apiToken->setUser(null);
+ }
+ }
+
+ return $this;
+ }
+
+
+ /*
+ * ====== 2FA
+ */
+
+ public function isTwoFactorEnabled(): bool
+ {
+ return ($this->twoFactor_active == true);
+ }
+
+ public function setTwoFactorEnabled(?bool $twoFactor_active): void
+ {
+ $this->twoFactor_active = $twoFactor_active;
+ }
+
+
+ public function isGoogleAuthenticatorEnabled(): bool
+ {
+ return (null !== $this->twoFactor_secret_google) and $this->isTwoFactorEnabled();
+ }
+
+ public function getGoogleAuthenticatorUsername(): string
+ {
+ return $this->username;
+ }
+
+ public function getGoogleAuthenticatorSecret(): ?string
+ {
+ return $this->twoFactor_secret_google;
+ }
+
+ public function setGoogleAuthenticatorSecret(?string $googleAuthenticatorSecret): void
+ {
+ $this->twoFactor_secret_google = $googleAuthenticatorSecret;
+ }
+
+ public function getBackupCodes(): ?array
+ {
+ return $this->twoFactor_backupCodes;
+ }
+ public function isBackupCode(string $code): bool
+ {
+ return in_array($code, $this->twoFactor_backupCodes);
+ }
+
+ public function invalidateBackupCode(string $code): void
+ {
+ $key = array_search($code, $this->twoFactor_backupCodes);
+ if ($key !== false) {
+ unset($this->twoFactor_backupCodes[$key]);
+ }
+ $this->generateBackUpCode();
+ }
+
+ public function invalidateAllBackupCodes(): void
+ {
+ $this->twoFactor_backupCodes = [];
+ }
+
+
+ public function addBackUpCode(string $backUpCode): void
+ {
+ if (!in_array($backUpCode, $this->twoFactor_backupCodes)) {
+ $this->twoFactor_backupCodes[] = $backUpCode;
+ }
+ }
+
+ public function generateBackUpCode($count=1): void
+ {
+ $codeLen = 6;
+ for ($i = 0; $i < $count; $i++) {
+ $backUpCode = substr(str_shuffle(str_repeat('0123456789', mt_rand(1, 10))), 1, $codeLen);
+ $this->addBackUpCode($backUpCode);
+ }
+ }
+
+ public function getTrustedTokenVersion(): int
+ {
+ return $this->twoFactor_trustedTokenVersion;
+ }
+}
diff --git a/app/src/Entity/UserRole.php b/app/src/Entity/UserRole.php
new file mode 100644
index 0000000..1d05178
--- /dev/null
+++ b/app/src/Entity/UserRole.php
@@ -0,0 +1,138 @@
+isActive = true;
+ $this->parentRole = null;
+ $this->systemrole = false;
+ }
+
+
+ public function __toString()
+ {
+ return (string) $this->role;
+ }
+
+ public function getId(): ?int
+ {
+ return $this->id;
+ }
+
+ public function getRole(): ?string
+ {
+ return $this->role;
+ }
+
+ public function setRole(string $role): self
+ {
+ $this->role = $role;
+
+ return $this;
+ }
+
+ public function getName(): ?string
+ {
+ return $this->name;
+ }
+
+ public function setName(string $name): self
+ {
+ $this->name = $name;
+
+ return $this;
+ }
+
+ public function getDescription(): ?string
+ {
+ return $this->description;
+ }
+
+ public function setDescription(string $description): self
+ {
+ $this->description = $description;
+
+ return $this;
+ }
+
+ public function isSystemrole(): ?bool
+ {
+ return $this->systemrole;
+ }
+
+ public function setSystemrole(bool $systemrole): self
+ {
+ $this->systemrole = $systemrole;
+
+ return $this;
+ }
+
+ public function getParentRole(): ?self
+ {
+ return $this->parentRole;
+ }
+
+ public function setParentRole(?self $parentRole): self
+ {
+ $this->parentRole = $parentRole;
+
+ return $this;
+ }
+
+ /*
+ * Recursive fetch all Parent Roles
+ */
+ public function getParentRoleRecursive(): ?array
+ {
+ $return = [];
+ $parent = $this->parentRole;
+ while ($parent) {
+ $return[] = $parent->getRole() ;
+ $parent = $parent->parentRole;
+ }
+ return $return;
+ }
+
+ public function getRoleAndParents(): ?array
+ {
+ return array_filter(array_unique(array_merge([$this->role], $this->getParentRoleRecursive())));
+ }
+}
diff --git a/app/src/EventListener/EntityUUIDListener.php b/app/src/EventListener/EntityUUIDListener.php
new file mode 100644
index 0000000..2df7639
--- /dev/null
+++ b/app/src/EventListener/EntityUUIDListener.php
@@ -0,0 +1,22 @@
+getEntity();
+
+
+ if (is_callable([$entity, 'generatePid'])) {
+ $entity->setPid($entity->generatePid());
+ }
+ }
+}
diff --git a/app/src/EventListener/LogoutSuccessEventListener.php b/app/src/EventListener/LogoutSuccessEventListener.php
new file mode 100644
index 0000000..f3bd519
--- /dev/null
+++ b/app/src/EventListener/LogoutSuccessEventListener.php
@@ -0,0 +1,20 @@
+log = $log;
+ }
+
+ public function onLogoutSuccess(LogoutEvent $logoutEvent): void
+ {
+ $user = $logoutEvent->getToken()->getUser();
+ $this->log->logout($user);
+ }
+}
diff --git a/app/src/Form/Core/ChangePasswordFormType.php b/app/src/Form/Core/ChangePasswordFormType.php
new file mode 100644
index 0000000..b36e495
--- /dev/null
+++ b/app/src/Form/Core/ChangePasswordFormType.php
@@ -0,0 +1,61 @@
+translator = $translator;
+ }
+
+ private function t($message, $params=[])
+ {
+ return $this->translator->trans($message, $params, 'core');
+ }
+
+ public function buildForm(FormBuilderInterface $builder, array $options): void
+ {
+ $builder
+ ->add('plainPassword', RepeatedType::class, [
+ 'type' => PasswordType::class,
+ 'invalid_message' => $this->t('service.registration.form.password.messages.no_match'),
+ 'options' => ['attr' => ['class' => 'password-field']],
+ 'required' => true,
+ 'first_options' => [ 'label' => $this->t('service.registration.form.password.label'),
+ 'attr' => [ 'class' => 'form-control'],
+ 'constraints' => [
+ new NotBlank([
+ 'message' => $this->t('service.registration.form.password.messages.blank'),
+ ]),
+ new Length([
+ 'min' => 6,
+ 'minMessage' => $this->t('service.registration.form.password.messages.min_lenght', ['limit' => 6]),
+ 'max' => 4096,
+ ]),
+ ],
+ ],
+ 'second_options' => [ 'label' => $this->t('service.registration.form.password.repeat_label'),
+ 'attr' => [ 'class' => 'form-control'],
+ ],
+ 'mapped' => false,
+ 'attr' => ['autocomplete' => 'new-password'],
+
+ ])
+ ;
+ }
+
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults([]);
+ }
+}
diff --git a/app/src/Form/Core/RegistrationFormType.php b/app/src/Form/Core/RegistrationFormType.php
new file mode 100644
index 0000000..b844fc9
--- /dev/null
+++ b/app/src/Form/Core/RegistrationFormType.php
@@ -0,0 +1,137 @@
+translator = $translator;
+ }
+
+ private function t($message, $params=[])
+ {
+ return $this->translator->trans($message, $params, 'core');
+ }
+
+ public function buildForm(FormBuilderInterface $builder, array $options): void
+ {
+ if ($options['ask_username']) {
+ $builder ->add('username', TextType::class, [
+ 'label' => $this->t('service.registration.form.username.label'),
+ 'attr' => [ 'placeholder' => $this->t('service.registration.form.username.placeholder'),
+ 'autocomplete' => 'username',
+ 'class' => 'form-control'
+ ],
+ ]);
+ }
+
+ if ($options['ask_name']) {
+ $builder ->add('firstname', TextType::class, [
+ 'label' => $this->t('service.registration.form.firstname.label'),
+ 'attr' => [ 'placeholder' => $this->t('service.registration.form.firstname.placeholder'),
+ 'autocomplete' => 'firstname',
+ 'class' => 'form-control'
+ ],
+ 'constraints' => [
+ new NotBlank([
+ 'message' => $this->t('service.registration.form.firstname.messages.blank'),
+ ]),
+ ],
+ ]);
+ $builder ->add('lastname', TextType::class, [
+ 'label' => $this->t('service.registration.form.lastname.label'),
+ 'attr' => [ 'placeholder' => $this->t('service.registration.form.lastname.placeholder'),
+ 'autocomplete' => 'lastname',
+ 'class' => 'form-control'
+ ],
+ 'constraints' => [
+ new NotBlank([
+ 'message' => $this->t('service.registration.form.lastname.messages.blank'),
+ ]),
+ ],
+ ]);
+ }
+
+ $builder
+ ->add('email', EmailType::class, [
+ 'label' => $this->t('service.registration.form.email.label'),
+ 'attr' => [ 'placeholder' => $this->t('service.registration.form.email.placeholder'),
+ 'autocomplete' => 'email',
+ 'class' => 'form-control'
+ ],
+ 'constraints' => [
+ new NotBlank([
+ 'message' => $this->t('service.registration.form.email.messages.blank'),
+ ]),
+ ],
+ ])
+ ->add('plainPassword', RepeatedType::class, [
+ 'type' => PasswordType::class,
+ 'invalid_message' => $this->t('service.registration.form.password.messages.no_match'),
+ 'options' => ['attr' => ['class' => 'password-field']],
+ 'required' => true,
+ 'first_options' => [ 'label' => $this->t('service.registration.form.password.label'),
+ 'attr' => [ 'class' => 'form-control'],
+ 'constraints' => [
+ new NotBlank([
+ 'message' => $this->t('service.registration.form.password.messages.blank'),
+ ]),
+ new Length([
+ 'min' => 6,
+ 'minMessage' => $this->t('service.registration.form.password.messages.min_lenght', ['limit' => 6]),
+ 'max' => 4096,
+ ]),
+ ],
+ ],
+ 'second_options' => [ 'label' => $this->t('service.registration.form.password.repeat_label'),
+ 'attr' => [ 'class' => 'form-control'],
+ ],
+ 'mapped' => false,
+ 'attr' => ['autocomplete' => 'new-password'],
+
+ ])
+ ->add('agreeTerms', CheckboxType::class, [
+ 'label' => $this->t('service.registration.form.terms.label'),
+ 'attr' => [ 'required' => 'true',
+ 'class' => 'form-check-input',
+ 'style' => 'width: 3em;',
+ 'role' => 'switch',
+ 'for' => 'term'
+ ],
+ 'mapped' => false,
+ 'constraints' => [
+ new IsTrue([
+ 'message' => $this->t('service.registration.form.terms.messages.agree'),
+ ]),
+ ],
+ ]);
+ }
+
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults([
+ 'data_class' => User::class,
+ 'ask_username' => false,
+ 'ask_name' => false,
+ 'fistname' => null,
+ 'lastname' => null,
+
+ ]);
+ }
+}
diff --git a/app/src/Form/Core/ResetPasswordRequestFormType.php b/app/src/Form/Core/ResetPasswordRequestFormType.php
new file mode 100644
index 0000000..5895fbf
--- /dev/null
+++ b/app/src/Form/Core/ResetPasswordRequestFormType.php
@@ -0,0 +1,45 @@
+translator = $translator;
+ }
+
+ private function t($message, $params=[])
+ {
+ return $this->translator->trans($message, $params, 'core');
+ }
+
+ public function buildForm(FormBuilderInterface $builder, array $options): void
+ {
+ $builder
+ ->add('email', EmailType::class, [
+ 'attr' => [ 'placeholder' => $this->t('service.registration.form.email.placeholder'),
+ 'autocomplete' => 'email',
+ 'class' => 'form-control'
+ ],
+ 'constraints' => [
+ new NotBlank([
+ 'message' => $this->t('service.registration.form.email.messages.blank'),
+ ]),
+ ],
+ ])
+ ;
+ }
+
+ public function configureOptions(OptionsResolver $resolver): void
+ {
+ $resolver->setDefaults([]);
+ }
+}
diff --git a/app/src/Kernel.php b/app/src/Kernel.php
new file mode 100644
index 0000000..d9a2e34
--- /dev/null
+++ b/app/src/Kernel.php
@@ -0,0 +1,17 @@
+getContainer()->getParameter('app')['timezone']);
+ }
+}
diff --git a/app/src/Repository/ApiTokenRepository.php b/app/src/Repository/ApiTokenRepository.php
new file mode 100644
index 0000000..2bd5a1d
--- /dev/null
+++ b/app/src/Repository/ApiTokenRepository.php
@@ -0,0 +1,66 @@
+
+ *
+ * @method ApiToken|null find($id, $lockMode = null, $lockVersion = null)
+ * @method ApiToken|null findOneBy(array $criteria, array $orderBy = null)
+ * @method ApiToken[] findAll()
+ * @method ApiToken[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
+ */
+class ApiTokenRepository extends ServiceEntityRepository
+{
+ public function __construct(ManagerRegistry $registry)
+ {
+ parent::__construct($registry, ApiToken::class);
+ }
+
+ public function add(ApiToken $entity, bool $flush = false): void
+ {
+ $this->getEntityManager()->persist($entity);
+
+ if ($flush) {
+ $this->getEntityManager()->flush();
+ }
+ }
+
+ public function remove(ApiToken $entity, bool $flush = false): void
+ {
+ $this->getEntityManager()->remove($entity);
+
+ if ($flush) {
+ $this->getEntityManager()->flush();
+ }
+ }
+
+// /**
+// * @return ApiToken[] Returns an array of ApiToken objects
+// */
+// public function findByExampleField($value): array
+// {
+// return $this->createQueryBuilder('a')
+// ->andWhere('a.exampleField = :val')
+// ->setParameter('val', $value)
+// ->orderBy('a.id', 'ASC')
+// ->setMaxResults(10)
+// ->getQuery()
+// ->getResult()
+// ;
+// }
+
+// public function findOneBySomeField($value): ?ApiToken
+// {
+// return $this->createQueryBuilder('a')
+// ->andWhere('a.exampleField = :val')
+// ->setParameter('val', $value)
+// ->getQuery()
+// ->getOneOrNullResult()
+// ;
+// }
+}
diff --git a/app/src/Repository/EmailRepository.php b/app/src/Repository/EmailRepository.php
new file mode 100644
index 0000000..bffbc2f
--- /dev/null
+++ b/app/src/Repository/EmailRepository.php
@@ -0,0 +1,66 @@
+
+ *
+ * @method Email|null find($id, $lockMode = null, $lockVersion = null)
+ * @method Email|null findOneBy(array $criteria, array $orderBy = null)
+ * @method Email[] findAll()
+ * @method Email[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
+ */
+class EmailRepository extends ServiceEntityRepository
+{
+ public function __construct(ManagerRegistry $registry)
+ {
+ parent::__construct($registry, Email::class);
+ }
+
+ public function add(Email $entity, bool $flush = false): void
+ {
+ $this->getEntityManager()->persist($entity);
+
+ if ($flush) {
+ $this->getEntityManager()->flush();
+ }
+ }
+
+ public function remove(Email $entity, bool $flush = false): void
+ {
+ $this->getEntityManager()->remove($entity);
+
+ if ($flush) {
+ $this->getEntityManager()->flush();
+ }
+ }
+
+// /**
+// * @return Email[] Returns an array of Email objects
+// */
+// public function findByExampleField($value): array
+// {
+// return $this->createQueryBuilder('e')
+// ->andWhere('e.exampleField = :val')
+// ->setParameter('val', $value)
+// ->orderBy('e.id', 'ASC')
+// ->setMaxResults(10)
+// ->getQuery()
+// ->getResult()
+// ;
+// }
+
+// public function findOneBySomeField($value): ?Email
+// {
+// return $this->createQueryBuilder('e')
+// ->andWhere('e.exampleField = :val')
+// ->setParameter('val', $value)
+// ->getQuery()
+// ->getOneOrNullResult()
+// ;
+// }
+}
diff --git a/app/src/Repository/LogRepository.php b/app/src/Repository/LogRepository.php
new file mode 100644
index 0000000..dddef7a
--- /dev/null
+++ b/app/src/Repository/LogRepository.php
@@ -0,0 +1,66 @@
+
+ *
+ * @method Log|null find($id, $lockMode = null, $lockVersion = null)
+ * @method Log|null findOneBy(array $criteria, array $orderBy = null)
+ * @method Log[] findAll()
+ * @method Log[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
+ */
+class LogRepository extends ServiceEntityRepository
+{
+ public function __construct(ManagerRegistry $registry)
+ {
+ parent::__construct($registry, Log::class);
+ }
+
+ public function add(Log $entity, bool $flush = false): void
+ {
+ $this->getEntityManager()->persist($entity);
+
+ if ($flush) {
+ $this->getEntityManager()->flush();
+ }
+ }
+
+ public function remove(Log $entity, bool $flush = false): void
+ {
+ $this->getEntityManager()->remove($entity);
+
+ if ($flush) {
+ $this->getEntityManager()->flush();
+ }
+ }
+
+// /**
+// * @return Log[] Returns an array of Log objects
+// */
+// public function findByExampleField($value): array
+// {
+// return $this->createQueryBuilder('l')
+// ->andWhere('l.exampleField = :val')
+// ->setParameter('val', $value)
+// ->orderBy('l.id', 'ASC')
+// ->setMaxResults(10)
+// ->getQuery()
+// ->getResult()
+// ;
+// }
+
+// public function findOneBySomeField($value): ?Log
+// {
+// return $this->createQueryBuilder('l')
+// ->andWhere('l.exampleField = :val')
+// ->setParameter('val', $value)
+// ->getQuery()
+// ->getOneOrNullResult()
+// ;
+// }
+}
diff --git a/app/src/Repository/ResetPasswordRequestRepository.php b/app/src/Repository/ResetPasswordRequestRepository.php
new file mode 100644
index 0000000..97724d6
--- /dev/null
+++ b/app/src/Repository/ResetPasswordRequestRepository.php
@@ -0,0 +1,51 @@
+
+ *
+ * @method ResetPasswordRequest|null find($id, $lockMode = null, $lockVersion = null)
+ * @method ResetPasswordRequest|null findOneBy(array $criteria, array $orderBy = null)
+ * @method ResetPasswordRequest[] findAll()
+ * @method ResetPasswordRequest[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
+ */
+class ResetPasswordRequestRepository extends ServiceEntityRepository implements ResetPasswordRequestRepositoryInterface
+{
+ use ResetPasswordRequestRepositoryTrait;
+
+ public function __construct(ManagerRegistry $registry)
+ {
+ parent::__construct($registry, ResetPasswordRequest::class);
+ }
+
+ public function add(ResetPasswordRequest $entity, bool $flush = false): void
+ {
+ $this->getEntityManager()->persist($entity);
+
+ if ($flush) {
+ $this->getEntityManager()->flush();
+ }
+ }
+
+ public function remove(ResetPasswordRequest $entity, bool $flush = false): void
+ {
+ $this->getEntityManager()->remove($entity);
+
+ if ($flush) {
+ $this->getEntityManager()->flush();
+ }
+ }
+
+ public function createResetPasswordRequest(object $user, \DateTimeInterface $expiresAt, string $selector, string $hashedToken): ResetPasswordRequestInterface
+ {
+ return new ResetPasswordRequest($user, $expiresAt, $selector, $hashedToken);
+ }
+}
diff --git a/app/src/Repository/UserRepository.php b/app/src/Repository/UserRepository.php
new file mode 100644
index 0000000..f061e35
--- /dev/null
+++ b/app/src/Repository/UserRepository.php
@@ -0,0 +1,93 @@
+
+ *
+ * @method User|null find($id, $lockMode = null, $lockVersion = null)
+ * @method User|null findOneBy(array $criteria, array $orderBy = null)
+ * @method User[] findAll()
+ * @method User[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
+ */
+class UserRepository extends ServiceEntityRepository implements PasswordUpgraderInterface
+{
+ public function __construct(ManagerRegistry $registry, ApiTokenRepository $apiTokenRepository)
+ {
+ parent::__construct($registry, User::class);
+ $this->apiTokenRepository = $apiTokenRepository;
+ }
+
+ public function add(User $entity, bool $flush = false): void
+ {
+ $this->getEntityManager()->persist($entity);
+
+ if ($flush) {
+ $this->getEntityManager()->flush();
+ }
+ }
+
+ public function remove(User $entity, bool $flush = false): void
+ {
+ $this->getEntityManager()->remove($entity);
+
+ if ($flush) {
+ $this->getEntityManager()->flush();
+ }
+ }
+
+ /**
+ * Used to upgrade (rehash) the user's password automatically over time.
+ */
+ public function upgradePassword(PasswordAuthenticatedUserInterface $user, string $newHashedPassword): void
+ {
+ if (!$user instanceof User) {
+ throw new UnsupportedUserException(sprintf('Instances of "%s" are not supported.', \get_class($user)));
+ }
+
+ $user->setPassword($newHashedPassword);
+
+ $this->add($user, true);
+ }
+
+ public function findByApiToken(string $token): ?User
+ {
+ return $this->apiTokenRepository->findOneBy([
+ 'token' => $token,
+ 'isActive' => true,
+ 'deletedAt' => null
+ ])?->getUser();
+ }
+
+// /**
+// * @return User[] Returns an array of User objects
+// */
+// public function findByExampleField($value): array
+// {
+// return $this->createQueryBuilder('u')
+// ->andWhere('u.exampleField = :val')
+// ->setParameter('val', $value)
+// ->orderBy('u.id', 'ASC')
+// ->setMaxResults(10)
+// ->getQuery()
+// ->getResult()
+// ;
+// }
+
+// public function findOneBySomeField($value): ?User
+// {
+// return $this->createQueryBuilder('u')
+// ->andWhere('u.exampleField = :val')
+// ->setParameter('val', $value)
+// ->getQuery()
+// ->getOneOrNullResult()
+// ;
+// }
+}
diff --git a/app/src/Repository/UserRoleRepository.php b/app/src/Repository/UserRoleRepository.php
new file mode 100644
index 0000000..23938dd
--- /dev/null
+++ b/app/src/Repository/UserRoleRepository.php
@@ -0,0 +1,54 @@
+
+ *
+ * @method UserRole|null find($id, $lockMode = null, $lockVersion = null)
+ * @method UserRole|null findOneBy(array $criteria, array $orderBy = null)
+ * @method UserRole[] findAll()
+ * @method UserRole[] findBy(array $criteria, array $orderBy = null, $limit = null, $offset = null)
+ */
+class UserRoleRepository extends ServiceEntityRepository
+{
+ public function __construct(ManagerRegistry $registry)
+ {
+ parent::__construct($registry, UserRole::class);
+ }
+
+ public function findAllActive()
+ {
+ return $this->findBy(array('isActive' => 1, 'deletedAt' => null), null);
+ }
+
+ public function getRoleAndParents(string $role): array
+ {
+ $role = $this->createQueryBuilder('r')
+ ->andWhere('r.role = :role')->andWhere('r.isActive = 1')->andWhere('r.deletedAt IS NULL')
+ ->setParameter('role', $role)
+ ->getQuery()->getResult();
+
+ if ($role[0]) {
+ $return = $role[0]->getRoleAndParents();
+ } else {
+ $this->addFlash('danger', $role.' is not active anymore , and was removed');
+ $return = [];
+ }
+ return $return;
+ }
+
+ public function getAllRolesToSave(array $roles)
+ {
+ $rolesToSave = [];
+ foreach ($roles as $r) {
+ $rolesToSave = array_merge($rolesToSave, $this->getRoleAndParents($r));
+ }
+
+ return array_filter(array_unique($rolesToSave));
+ }
+}
diff --git a/app/src/Ressources/templates/bundles/EasyAdminBundle/fields/array_readonly.html.twig b/app/src/Ressources/templates/bundles/EasyAdminBundle/fields/array_readonly.html.twig
new file mode 100644
index 0000000..ae6e996
--- /dev/null
+++ b/app/src/Ressources/templates/bundles/EasyAdminBundle/fields/array_readonly.html.twig
@@ -0,0 +1,16 @@
+{# @var ea \EasyCorp\Bundle\EasyAdminBundle\Context\AdminContext #}
+{# @var field \EasyCorp\Bundle\EasyAdminBundle\Dto\FieldDto #}
+{# @var entity \EasyCorp\Bundle\EasyAdminBundle\Dto\EntityDto #}
+
+XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
+
+{% if ea.crud.currentAction == 'detail' %}
+
+ {% for item in field.value %}
+ {{ item }}
+ {% endfor %}
+
+{% else %}
+
+ {{ field.formattedValue }}
+{% endif %}
diff --git a/app/src/Ressources/templates/bundles/EasyAdminBundle/pages/dashboard.html.twig b/app/src/Ressources/templates/bundles/EasyAdminBundle/pages/dashboard.html.twig
new file mode 100644
index 0000000..0d621c5
--- /dev/null
+++ b/app/src/Ressources/templates/bundles/EasyAdminBundle/pages/dashboard.html.twig
@@ -0,0 +1,12 @@
+{% extends '@EasyAdmin/page/content.html.twig' %}
+
+{% block page_title 'Welcome to the Dashboard' %}
+
+{% block page_content %}
+ You find this page here: src/Ressources/templates/bundles/EasyAdminBundle/pages/dashboard.html.twig
+
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Amet justo donec enim diam vulputate ut. Id ornare arcu odio ut sem nulla pharetra diam. Accumsan tortor posuere ac ut consequat. Viverra accumsan in nisl nisi scelerisque eu ultrices vitae auctor. Volutpat blandit aliquam etiam erat velit. Vel quam elementum pulvinar etiam. Velit scelerisque in dictum non. Dui id ornare arcu odio. Elementum curabitur vitae nunc sed velit. Vitae congue eu consequat ac felis donec et. Malesuada pellentesque elit eget gravida cum sociis. Nisi quis eleifend quam adipiscing vitae. Euismod in pellentesque massa placerat duis ultricies lacus sed. Senectus et netus et malesuada fames ac turpis egestas integer. Eros donec ac odio tempor orci dapibus ultrices in iaculis.
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/app/src/Ressources/templates/bundles/EasyAdminBundle/pages/phpinfo.html.twig b/app/src/Ressources/templates/bundles/EasyAdminBundle/pages/phpinfo.html.twig
new file mode 100644
index 0000000..104576c
--- /dev/null
+++ b/app/src/Ressources/templates/bundles/EasyAdminBundle/pages/phpinfo.html.twig
@@ -0,0 +1,5 @@
+{% extends '@EasyAdmin/page/content.html.twig' %}
+
+{% block page_content %}
+ {{ phpinfo | raw }}
+{% endblock %}
\ No newline at end of file
diff --git a/app/src/Ressources/templates/email/base.email.html.twig b/app/src/Ressources/templates/email/base.email.html.twig
new file mode 100644
index 0000000..4d6d734
--- /dev/null
+++ b/app/src/Ressources/templates/email/base.email.html.twig
@@ -0,0 +1,35 @@
+{% trans_default_domain 'core' %}
+
+
+
+
+ {% block email_subject %}Subject{% endblock %}
+
+
+
+
+
+
+
+
+ {% block email_header %}
+ {% include 'email/include/header.email.html.twig' %}
+ {% endblock %}
+
+
+ {% block email_body %}
+ {% embed 'email/include/body.email.html.twig' %}{% endembed %}
+ {% endblock %}
+
+
+ {% block email_footer %}
+ {% include 'email/include/footer.email.html.twig' %}
+ {% endblock %}
+
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/Ressources/templates/email/core/registration_confirmation_email.html.twig b/app/src/Ressources/templates/email/core/registration_confirmation_email.html.twig
new file mode 100644
index 0000000..12fd981
--- /dev/null
+++ b/app/src/Ressources/templates/email/core/registration_confirmation_email.html.twig
@@ -0,0 +1,37 @@
+{% extends 'email/base.email.html.twig' %}
+{% trans_default_domain 'core' %}
+
+{% block email_body %}
+ {% embed 'email/include/body.email.html.twig' %}
+ {% trans_default_domain 'core' %}
+ {% set email_body_title = 'service.registration.email.body_title' | trans %}
+
+ {% block email_body_content %}
+
+ {{ 'service.generic.email.greeting' | trans }} {{ greeetingName }}
+
+
+ {{ 'service.registration.email.body_text' | trans }}
+
+
+ {% include 'email/include/spacingTable.html.twig' %}
+
+
+ {% set btnURL = signedUrl|raw %}
+ {% set btnText = 'service.registration.email.button_text' | trans %}
+ {% include 'email/include/button.html.twig' with {'btnURL': btnURL, 'btnText' : btnText} %}
+
+
+ {% include 'email/include/spacingTable.html.twig' %}
+
+
+ {{ 'service.generic.expire'| trans({'expire' : expiresAtMessageKey|trans(expiresAtMessageData, 'VerifyEmailBundle')}) }}
+
+
+
+ {{ 'service.generic.email.farewell' | trans }}
+
+
+ {% endblock email_body_content %}
+ {% endembed %}
+{% endblock email_body %}
\ No newline at end of file
diff --git a/app/src/Ressources/templates/email/core/reset_password.email.html.twig b/app/src/Ressources/templates/email/core/reset_password.email.html.twig
new file mode 100644
index 0000000..294ece0
--- /dev/null
+++ b/app/src/Ressources/templates/email/core/reset_password.email.html.twig
@@ -0,0 +1,38 @@
+{% extends 'email/base.email.html.twig' %}
+{% trans_default_domain 'core' %}
+
+
+{% block email_body %}
+ {% embed 'email/include/body.email.html.twig' %}
+ {% trans_default_domain 'core' %}
+ {% set email_body_title = 'service.password_reset.email.body_title'|trans %}
+
+ {% block email_body_content %}
+
+ {{ 'service.generic.email.greeting'|trans }} {{ greeetingName }}
+
+
+ {{ 'service.password_reset.email.body_text'|trans }}
+
+
+ {% include 'email/include/spacingTable.html.twig' %}
+
+
+ {% set btnURL = url('app_reset_password', {token: resetToken.token}) %}
+ {% set btnText = 'service.password_reset.email.button_text'|trans %}
+ {% include 'email/include/button.html.twig' with {'btnURL': btnURL, 'btnText' : btnText} %}
+
+
+ {% include 'email/include/spacingTable.html.twig' %}
+
+
+ {{ 'service.generic.expire'|trans({'expire' : resetToken.expirationMessageKey|trans(resetToken.expirationMessageData, 'ResetPasswordBundle')}) }}
+
+
+
+ {{ 'service.generic.email.farewell'|trans }}
+
+
+ {% endblock email_body_content %}
+ {% endembed %}
+{% endblock email_body %}
diff --git a/app/src/Ressources/templates/email/include/body.email.html.twig b/app/src/Ressources/templates/email/include/body.email.html.twig
new file mode 100644
index 0000000..c176140
--- /dev/null
+++ b/app/src/Ressources/templates/email/include/body.email.html.twig
@@ -0,0 +1,31 @@
+{% trans_default_domain 'core' %}
+
+{% if email_body_title is not defined %}
+ {% set email_body_title = 'Lorem ipsum' %}
+{% endif %}
+
+
+
+ {% if email_body_title %}
+
+
+ {{ email_body_title }}
+
+
+ {% endif %}
+
+
+ {% block email_body_content %}
+
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Cras viverra quis leo vitae molestie.
+ Nullam imperdiet massa quis cursus luctus. Ut auctor pulvinar enim, eu luctus urna posuere sit amet.
+ Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec quis gravida diam, sed laoreet quam.
+ Praesent volutpat, quam nec ultricies cursus, enim elit cursus sem, nec porta enim enim id risus.
+ Fusce facilisis quam eu arcu sollicitudin, in facilisis massa accumsan.
+ Aenean dapibus nisi dui, vitae ullamcorper est viverra vel.
+ Donec non arcu tincidunt, egestas ex ac, tempor nisi. Nulla suscipit pellentesque arcu sit amet fermentum.
+
+ {% endblock %}
+
+
+
\ No newline at end of file
diff --git a/app/src/Ressources/templates/email/include/button.html.twig b/app/src/Ressources/templates/email/include/button.html.twig
new file mode 100644
index 0000000..9dd2015
--- /dev/null
+++ b/app/src/Ressources/templates/email/include/button.html.twig
@@ -0,0 +1,34 @@
+{% if btnBackgroundColor is not defined or btnBackgroundColor is null %}
+ {% set btnBackgroundColor = '#6174d1' %}
+{% endif %}
+
+{% if btnFontColor is not defined or btnFontColor is null %}
+ {% set btnFontColor = '#FFFFFF' %}
+{% endif %}
+
+{% if btnURL is not defined or btnURL is null %}
+ {% set btnURL = '#noURLdefined' %}
+{% endif %}
+
+{% if btnText is not defined or btnText is null %}
+ {% set btnText = '#missingButtonText' %}
+{% endif %}
+
+
+
+
+
+
+
+
diff --git a/app/src/Ressources/templates/email/include/footer.email.html.twig b/app/src/Ressources/templates/email/include/footer.email.html.twig
new file mode 100644
index 0000000..54dd0bc
--- /dev/null
+++ b/app/src/Ressources/templates/email/include/footer.email.html.twig
@@ -0,0 +1,22 @@
+{% trans_default_domain 'core' %}
+{% if footer_background_color is not defined or footer_background_color is null %}
+ {%set footer_background_color = '#ee4c50' %}
+{% endif %}
+{% if footer_font_color is not defined or footer_font_color is null %}
+ {%set footer_font_color = '#ffffff' %}
+{% endif %}
+
+
+
+
+
+
+
+ {% block email_footer_content %}
+ {{ 'service.generic.email.footer'|trans }}
+ {% endblock %}
+
+
+
+
+
\ No newline at end of file
diff --git a/app/src/Ressources/templates/email/include/header.email.html.twig b/app/src/Ressources/templates/email/include/header.email.html.twig
new file mode 100644
index 0000000..9055c4f
--- /dev/null
+++ b/app/src/Ressources/templates/email/include/header.email.html.twig
@@ -0,0 +1,15 @@
+{% trans_default_domain 'core' %}
+{% if header_background_color is not defined or header_background_color is null %}
+ {%set header_background_color = '#70bbd9' %}
+{% endif %}
+{% if header_font_color is not defined or header_font_color is null %}
+ {%set header_font_color = '#000000' %}
+{% endif %}
+
+
+
+ {%block email_header_content %}
+ {{ 'service.generic.email.header'|trans|raw }}
+ {% endblock %}
+
+
\ No newline at end of file
diff --git a/app/src/Ressources/templates/email/include/spacingTable.html.twig b/app/src/Ressources/templates/email/include/spacingTable.html.twig
new file mode 100644
index 0000000..8c249d5
--- /dev/null
+++ b/app/src/Ressources/templates/email/include/spacingTable.html.twig
@@ -0,0 +1,6 @@
+
\ No newline at end of file
diff --git a/app/src/Ressources/templates/email/include/stylesButton.html.twig b/app/src/Ressources/templates/email/include/stylesButton.html.twig
new file mode 100644
index 0000000..d615275
--- /dev/null
+++ b/app/src/Ressources/templates/email/include/stylesButton.html.twig
@@ -0,0 +1,17 @@
+color: {{btnFontColor}};
+background-color: {{btnBackgroundColor}};
+strokecolor: {{btnBackgroundColor}};
+fillcolor: {{btnBackgroundColor}};
+border: 0px solid {{btnBackgroundColor}};
+border-radius:4px;
+display:inline-block;
+font-family: Arial, Helvetica;
+font-size:15pt;
+font-weight:bold;
+line-height:40px;
+text-align: center;
+vertical-align: middle;
+text-decoration:none;
+width:75%;
+min-height:40px;
+-webkit-text-size-adjust:none;
diff --git a/app/src/Ressources/templates/email/include/stylesButtonMso.html.twig b/app/src/Ressources/templates/email/include/stylesButtonMso.html.twig
new file mode 100644
index 0000000..4075b9a
--- /dev/null
+++ b/app/src/Ressources/templates/email/include/stylesButtonMso.html.twig
@@ -0,0 +1,4 @@
+height:40px;
+min-height:40px;
+v-text-anchor:middle;
+width:300px;
\ No newline at end of file
diff --git a/app/src/Ressources/templates/email/test_email.html.twig b/app/src/Ressources/templates/email/test_email.html.twig
new file mode 100644
index 0000000..ef23121
--- /dev/null
+++ b/app/src/Ressources/templates/email/test_email.html.twig
@@ -0,0 +1,15 @@
+{% extends 'email/base.email.html.twig' %}
+{% trans_default_domain 'core' %}
+
+
+{% block email_body %}
+ {% embed 'email/include/body.email.html.twig' %}
+ {% trans_default_domain 'core' %}
+
+ {% set email_body_title = 'This is a test title' %}
+
+ {% block email_body_content %}
+ This is the email body, with a link
+ {% endblock email_body_content %}
+ {% endembed %}
+{% endblock email_body %}
diff --git a/app/src/Ressources/templates/view/base.html.twig b/app/src/Ressources/templates/view/base.html.twig
new file mode 100644
index 0000000..f3e6710
--- /dev/null
+++ b/app/src/Ressources/templates/view/base.html.twig
@@ -0,0 +1,19 @@
+
+
+
+
+ {% block page_title %}{% endblock %}
+
+ {# Run `composer require symfony/webpack-encore-bundle` to start using Symfony UX #}
+ {% block stylesheets %}
+
+ {{ encore_entry_link_tags('app') }}
+ {% endblock %}
+ {% block javascripts %}
+ {{ encore_entry_script_tags('app') }}
+ {% endblock %}
+
+
+ {% block body %}{% endblock %}
+
+
diff --git a/app/src/Ressources/templates/view/core/2fa/enable2fa.html.twig b/app/src/Ressources/templates/view/core/2fa/enable2fa.html.twig
new file mode 100644
index 0000000..d6b1b8a
--- /dev/null
+++ b/app/src/Ressources/templates/view/core/2fa/enable2fa.html.twig
@@ -0,0 +1,141 @@
+{% extends '@EasyAdmin/page/content.html.twig' %}
+{% trans_default_domain 'admin' %}
+
+
+{% block page_title 'Two Factor Authentication' %}
+{% block content_title '' %}
+
+
+
+{% block page_content %}
+
+
+
+
+ {% if isEnabled %}
+
+
+
+ {{ 'admin.crud.user.twofactor.badge.enabled'| trans }}
+
+
+
+ {{ 'admin.crud.user.twofactor.label.link_disable_intro'| trans }}
+ {{ 'admin.crud.user.twofactor.label.link_disable'| trans }} .
+
+
+
+ {% else %}
+
+
+ {{ 'admin.crud.user.twofactor.badge.disabled'| trans }}
+
+
+
+
+ {{ 'admin.crud.user.twofactor.label.explain_paragraph'| trans }}
+
+
+
+ {{ 'admin.crud.user.twofactor.label.apps_intro'| trans }}
+
+
+
+
+
+ {% endif %}
+
+
+ {% if not isEnabled and isLoggedInUser %}
+
+
+
+
{{ 'admin.crud.user.twofactor.label.step2'| trans }}
+
+
+
+
+
+
+
+
{{ 'admin.crud.user.twofactor.label.step2'| trans }}
+
+
+
+
+
+
+ {% endif %}
+
+
+
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/app/src/Ressources/templates/view/core/2fa/enable2fa_crud.html.twig b/app/src/Ressources/templates/view/core/2fa/enable2fa_crud.html.twig
new file mode 100644
index 0000000..9f64ddd
--- /dev/null
+++ b/app/src/Ressources/templates/view/core/2fa/enable2fa_crud.html.twig
@@ -0,0 +1,64 @@
+
+{% trans_default_domain 'admin' %}
+
+
+
+
+ {% if isEnabled %}
+
+
+ {{ 'admin.crud.user.twofactor.badge.enabled'| trans }}
+
+
+
+ {% if isLoggedInUser %}
+
+
+
+
+ {% if backupCodes|default(false) %}
+
{{ 'admin.crud.user.twofactor.backup.title'| trans }}
+
{{ 'admin.crud.user.twofactor.backup.intro'| trans }}
+
+
+ {% for c in backupCodes %}
+ {{c}}
+ {% endfor %}
+
+
+ {% endif %}
+ {% endif %}
+
+
+
+
+ {% else %}
+
+
+ {{ 'admin.crud.user.twofactor.badge.disabled'| trans }}
+
+
+
+ {% if isLoggedInUser %}
+
+ {{ 'admin.crud.user.twofactor.label.explain_paragraph'| trans }}
+
+
+
+
+
+ {% endif %}
+ {% endif %}
+
+
+
diff --git a/app/src/Ressources/templates/view/core/2fa/login_2fa_form.html.twig b/app/src/Ressources/templates/view/core/2fa/login_2fa_form.html.twig
new file mode 100644
index 0000000..f50da09
--- /dev/null
+++ b/app/src/Ressources/templates/view/core/2fa/login_2fa_form.html.twig
@@ -0,0 +1,86 @@
+{% extends 'view/core/core_base.html.twig' %}
+{% trans_default_domain 'core' %}
+
+{% set page_title = 'service.login.twofactor.page_title'|trans %}
+{% block title %}{{'service.login.twofactor.page_title'|trans}}{% endblock %}
+
+{% block login_alert %}
+ {% if authenticationError %}
+
+
{{ authenticationError|trans(authenticationErrorData, 'SchebTwoFactorBundle') }}
+
+ {% endif %}
+{% endblock %}
+
+{% block page_content %}
+ {% block login_form %}
+
+ {{ 'service.login.twofactor.info_text'|trans }}
+
+
+ {% if authenticationError %}
+ {{ authenticationError|trans(authenticationErrorData, 'SchebTwoFactorBundle') }}
+ {% endif %}
+
+ {# Let the user select the authentication method #}
+ {% if availableTwoFactorProviders|length > 1 %}
+ {{ "choose_provider"|trans({}, 'SchebTwoFactorBundle') }}:
+ {% for provider in availableTwoFactorProviders %}
+ {{ provider }}
+ {% endfor %}
+
+ {% endif %}
+
+
+
+ {# The logout link gives the user a way out if they can't complete two-factor authentication #}
+
+
+
+ {% endblock %}
+{% endblock %}
diff --git a/app/src/Ressources/templates/view/core/core_base.html.twig b/app/src/Ressources/templates/view/core/core_base.html.twig
new file mode 100644
index 0000000..550c7c9
--- /dev/null
+++ b/app/src/Ressources/templates/view/core/core_base.html.twig
@@ -0,0 +1,37 @@
+{% extends '@!EasyAdmin/page/login.html.twig' %}
+{% trans_default_domain 'core' %}
+
+{% if page_title is not defined or page_title is empty %}
+ {% set page_title = 'missing page_title' %}
+{% endif %}
+
+{% block wrapper_wrapper %}
+
+
+
+ {% block login_alert %}
+ {% if error|default(false) %}
+
+ {{ error.messageKey|trans(error.messageData, 'security') }}
+
+ {% endif %}
+ {% endblock %}
+
+
+ {% block page_content %}
+ Empty block page_content
+ {% endblock %}
+
+
+
+{% endblock %}
\ No newline at end of file
diff --git a/app/src/Ressources/templates/view/core/login/login.html.twig b/app/src/Ressources/templates/view/core/login/login.html.twig
new file mode 100644
index 0000000..deceb4c
--- /dev/null
+++ b/app/src/Ressources/templates/view/core/login/login.html.twig
@@ -0,0 +1,93 @@
+{% extends 'view/core/core_base.html.twig' %}
+{% trans_default_domain 'core' %}
+
+{% set page_title = 'service.login.page.page_title' | trans %}
+
+{% block login_alert %}
+ {% if error|default(false) %}
+ {% if error.messageKey == 'login_email_not_verified' %}
+
+ {{ 'service.verify_mail.message.not_verified'|trans({'url': path('app_request_verify_email')})|raw }}
+
+ {% else %}
+
+ {{ error.messageKey|trans(error.messageData, 'security') }}
+
+ {% endif %}
+
+ {% endif %}
+{% endblock %}
+
+{% block page_content %}
+ {% block login_form %}
+
+
+ {% if registrationActive %}
+
+
+
+ {% endif %}
+ {% if passwordResetActive %}
+
+
+
+ {% endif %}
+
+ {% endblock %}
+{% endblock %}
\ No newline at end of file
diff --git a/app/src/Ressources/templates/view/core/login/login_raw.html.twig b/app/src/Ressources/templates/view/core/login/login_raw.html.twig
new file mode 100644
index 0000000..af85114
--- /dev/null
+++ b/app/src/Ressources/templates/view/core/login/login_raw.html.twig
@@ -0,0 +1,44 @@
+{% extends 'view/base.html.twig' %}
+
+{% block title %}Log in!{% endblock %}
+
+{% block body %}
+
+
+{% endblock %}
+
diff --git a/app/src/Ressources/templates/view/core/registration/register.html.twig b/app/src/Ressources/templates/view/core/registration/register.html.twig
new file mode 100644
index 0000000..212b2ae
--- /dev/null
+++ b/app/src/Ressources/templates/view/core/registration/register.html.twig
@@ -0,0 +1,103 @@
+{% extends 'view/core/core_base.html.twig' %}
+{% trans_default_domain 'core' %}
+
+{% set page_title ='service.registration.page.page_title' | trans %}
+
+{% block page_content %}
+ {% block login_form %}
+ {% for flashError in app.flashes('verify_email_error') %}
+ {{ flashError }}
+ {% endfor %}
+
+ {% if mailSent|default(false) %}
+
+ {{ 'service.registration.page.header_mailsent_text' | trans }}
+
+
+
+
+
+ {% else %}
+ {{ form_start(registrationForm) }}
+
+ {{ 'service.registration.page.header_text' | trans }}
+
+
+
+ {% if askUsername %}
+
+
{{ 'service.registration.form.username.label' | trans }}
+
+ {{ form_row(registrationForm.username) }}
+
+
+ {% endif %}
+
+ {% if askName %}
+
+
{{ 'service.registration.form.firstname.label' | trans }}
+
+ {{ form_row(registrationForm.firstname) }}
+
+
+
+
{{ 'service.registration.form.lastname.label' | trans }}
+
+ {{ form_row(registrationForm.lastname) }}
+
+
+ {% endif %}
+
+
+
+
{{ 'service.registration.form.email.label' | trans }}
+
+ {{ form_row(registrationForm.email) }}
+
+
+
+
+
{{ 'service.registration.form.password.label' | trans }}
+
+ {{ form_row(registrationForm.plainPassword) }}
+
+
+
+
+
+
+
+
+
+ {{ 'service.registration.form.submit.label' | trans }}
+
+
+ {{ form_end(registrationForm) }}
+
+
+
+ {% endif %}
+ {% endblock %}
+{% endblock %}
+
+
+
\ No newline at end of file
diff --git a/app/src/Ressources/templates/view/core/registration/register_raw.html.twig b/app/src/Ressources/templates/view/core/registration/register_raw.html.twig
new file mode 100644
index 0000000..887d475
--- /dev/null
+++ b/app/src/Ressources/templates/view/core/registration/register_raw.html.twig
@@ -0,0 +1,19 @@
+{% extends 'views/base.html.twig' %}
+
+{% block title %}Register{% endblock %}
+
+{% block body %}
+ {% for flash_error in app.flashes('verify_email_error') %}
+ {{ flash_error }}
+ {% endfor %}
+
+ Register
+
+ {{ form_start(registrationForm) }}
+ {{ form_row(registrationForm.email) }}
+ {{ form_row(registrationForm.plainPassword}) }}
+ {{ form_row(registrationForm.agreeTerms) }}
+
+ Register
+ {{ form_end(registrationForm) }}
+{% endblock %}
diff --git a/app/src/Ressources/templates/view/core/resend_verifcation/resend_verifcation.html.twig b/app/src/Ressources/templates/view/core/resend_verifcation/resend_verifcation.html.twig
new file mode 100644
index 0000000..3911a2d
--- /dev/null
+++ b/app/src/Ressources/templates/view/core/resend_verifcation/resend_verifcation.html.twig
@@ -0,0 +1,37 @@
+{% extends 'view/core/core_base.html.twig' %}
+{% trans_default_domain 'core' %}
+
+{% set page_title ='service.verify_mail.page.title' | trans %}
+
+{% block page_content %}
+ {% block login_form %}
+ {{ form_start(requestForm) }}
+
+
+ {{ 'service.verify_mail.page.text' | trans }}
+
+
+
{{ 'service.registration.form.email.label' | trans }}
+
+ {{ form_widget(requestForm.email) }}
+
+
+
+
+
+ {{ 'service.verify_mail.page.submit_label' | trans }}
+
+
+ {{ form_end(requestForm) }}
+
+
+
+ {% endblock %}
+{% endblock %}
+
+
+
\ No newline at end of file
diff --git a/app/src/Ressources/templates/view/core/reset_password/check_email.html.twig b/app/src/Ressources/templates/view/core/reset_password/check_email.html.twig
new file mode 100644
index 0000000..cb6a807
--- /dev/null
+++ b/app/src/Ressources/templates/view/core/reset_password/check_email.html.twig
@@ -0,0 +1,28 @@
+{% extends 'view/core/core_base.html.twig' %}
+{% trans_default_domain 'core' %}
+
+{% set page_title = 'service.password_reset.page_sent.title' | trans %}
+
+{% block page_content %}
+ {% block login_form %}
+
+
+ {{ 'service.password_reset.page_sent.text_intro' | trans }}
+ {{ 'service.generic.expire'| trans({'expire' : resetToken.expirationMessageKey|trans(resetToken.expirationMessageData, 'ResetPasswordBundle')}) }}
+
+
+ {{ 'service.password_reset.page_sent.text_outro'| trans({'url' : path('app_forgot_password_request') })|raw }}
+
+
+
+
+
+ {% endblock %}
+{% endblock %}
+
+
+
\ No newline at end of file
diff --git a/app/src/Ressources/templates/view/core/reset_password/check_email_raw.html.twig b/app/src/Ressources/templates/view/core/reset_password/check_email_raw.html.twig
new file mode 100644
index 0000000..74032a1
--- /dev/null
+++ b/app/src/Ressources/templates/view/core/reset_password/check_email_raw.html.twig
@@ -0,0 +1,11 @@
+{% extends 'view/base.html.twig' %}
+
+{% block title %}Password Reset Email Sent{% endblock %}
+
+{% block body %}
+
+ If an account matching your email exists, then an email was just sent that contains a link that you can use to reset your password.
+ This link will expire in {{ resetToken.expirationMessageKey|trans(resetToken.expirationMessageData, 'ResetPasswordBundle') }}.
+
+ If you don't receive an email please check your spam folder or try again .
+{% endblock %}
diff --git a/app/src/Ressources/templates/view/core/reset_password/request.html.twig b/app/src/Ressources/templates/view/core/reset_password/request.html.twig
new file mode 100644
index 0000000..631faca
--- /dev/null
+++ b/app/src/Ressources/templates/view/core/reset_password/request.html.twig
@@ -0,0 +1,36 @@
+{% extends 'view/core/core_base.html.twig' %}
+{% trans_default_domain 'core' %}
+
+{% set page_title ='service.password_reset.page.title' | trans %}
+
+{% block page_content %}
+ {% block login_form %}
+ {{ form_start(requestForm) }}
+
+ {{ 'service.password_reset.page.text' | trans }}
+
+
+
{{ 'service.registration.form.email.label' | trans }}
+
+ {{ form_widget(requestForm.email) }}
+
+
+
+
+
+ {{ 'service.password_reset.page.submit_label' | trans }}
+
+
+ {{ form_end(requestForm) }}
+
+
+
+ {% endblock %}
+{% endblock %}
+
+
+
\ No newline at end of file
diff --git a/app/src/Ressources/templates/view/core/reset_password/request_raw.html.twig b/app/src/Ressources/templates/view/core/reset_password/request_raw.html.twig
new file mode 100644
index 0000000..6f33777
--- /dev/null
+++ b/app/src/Ressources/templates/view/core/reset_password/request_raw.html.twig
@@ -0,0 +1,22 @@
+{% extends 'view/base.html.twig' %}
+
+{% block title %}Reset your password{% endblock %}
+
+{% block body %}
+ {% for flash_error in app.flashes('reset_password_error') %}
+ {{ flash_error }}
+ {% endfor %}
+ Reset your password
+
+ {{ form_start(requestForm) }}
+ {{ form_row(requestForm.email) }}
+
+
+ Enter your email address and we will send you a
+ link to reset your password.
+
+
+
+ Send password reset email
+ {{ form_end(requestForm) }}
+{% endblock %}
diff --git a/app/src/Ressources/templates/view/core/reset_password/reset.html.twig b/app/src/Ressources/templates/view/core/reset_password/reset.html.twig
new file mode 100644
index 0000000..b8c3d67
--- /dev/null
+++ b/app/src/Ressources/templates/view/core/reset_password/reset.html.twig
@@ -0,0 +1,30 @@
+{% extends 'view/core/core_base.html.twig' %}
+{% trans_default_domain 'core' %}
+
+{% set page_title ='service.password_reset.page_reset.title' | trans %}
+
+{% block page_content %}
+ {% block login_form %}
+ {{ form_start(resetForm) }}
+
+
+ {{ 'service.password_reset.page_reset.text' | trans }}
+
+
+
{{ 'service.registration.form.password.label' | trans }}
+
+ {{ form_row(resetForm.plainPassword) }}
+
+
+
+
+ {{ 'service.password_reset.page_reset.submit_label' | trans }}
+
+
+
+ {{ form_end(resetForm) }}
+ {% endblock %}
+{% endblock %}
+
+
+
\ No newline at end of file
diff --git a/app/src/Ressources/templates/view/core/reset_password/reset_raw.html.twig b/app/src/Ressources/templates/view/core/reset_password/reset_raw.html.twig
new file mode 100644
index 0000000..7b8a286
--- /dev/null
+++ b/app/src/Ressources/templates/view/core/reset_password/reset_raw.html.twig
@@ -0,0 +1,12 @@
+{% extends 'view/base.html.twig' %}
+
+{% block title %}Reset your password{% endblock %}
+
+{% block body %}
+ Reset your password
+
+ {{ form_start(resetForm) }}
+ {{ form_row(resetForm.plainPassword) }}
+ Reset password
+ {{ form_end(resetForm) }}
+{% endblock %}
diff --git a/app/src/Ressources/templates/view/core/terms/terms.html.twig b/app/src/Ressources/templates/view/core/terms/terms.html.twig
new file mode 100644
index 0000000..6677c17
--- /dev/null
+++ b/app/src/Ressources/templates/view/core/terms/terms.html.twig
@@ -0,0 +1,73 @@
+{% extends 'view/base.html.twig' %}
+{% trans_default_domain "tos" %}
+
+{% block page_title %}{{ 'basics.title.page'|trans }}{% endblock %}
+{% block body %}
+
+
+
+
+
+
{{ 'basics.title.tos'|trans }}
+
+ {% block content %}
+ {% set hasChapterContent = false %}
+ {% for c in 1..50 %}
+ {% set hasChapterContent = false %}
+ {% set chapter_title = 'chapter_' ~ c ~ '.title' %}
+ {% set chapter_text = 'chapter_' ~ c ~ '.text' %}
+ {% set chapter_next = 'chapter_' ~ (c+1) ~ '.text' %}
+ {% set chapter_break = chapter_next|trans != chapter_next %}
+
+ {% if chapter_title|trans != chapter_title %}
+ {{ c }}. {{ chapter_title|trans }}
+ {% endif %}
+ {% if chapter_text|trans != chapter_text %}
+ {{ chapter_text|trans|raw }}
+ {% set hasChapterContent = true %}
+ {% endif %}
+
+ {% for sc in 1..30 %}
+ {% set subchapter_title = 'chapter_' ~ c ~ '.subchapter_' ~ sc ~ '.title' %}
+ {% set subchapter_text = 'chapter_' ~ c ~ '.subchapter_' ~ sc ~ '.text' %}
+ {% set subchapter_break = (subchapter_title|trans == subchapter_title) %}
+
+ {% if not subchapter_break %}
+ {% if subchapter_title|trans != subchapter_title %}
+ {{ c }}.{{ sc }}. {{ subchapter_title|trans }}
+ {% endif %}
+ {% if subchapter_text|trans != subchapter_text %}
+ {{ subchapter_text|trans|raw }}
+ {% set hasChapterContent = true %}
+ {% endif %}
+
+ {% for ssc in 1..10 %}
+ {% set subsubchapter_title = 'chapter_' ~ c ~ '.subchapter_' ~ sc ~ '.subsubchapter_' ~ ssc ~ '.title' %}
+ {% set subsubchapter_text = 'chapter_' ~ c ~ '.subchapter_' ~ sc ~ '.subsubchapter_' ~ ssc ~ '.text' %}
+ {% set subsubchapter_break = (subsubchapter_text|trans == subsubchapter_text) %}
+
+ {% if not subsubchapter_break %}
+ {% if subsubchapter_title|trans != subsubchapter_title %}
+ {{ c }}.{{ sc }}.{{ ssc }}. {{ subsubchapter_title|trans }}
+ {% endif %}
+ {% if subsubchapter_text|trans != subsubchapter_text %}
+ {{ subsubchapter_text|trans|raw }}
+ {% set hasChapterContent = true %}
+ {% endif %}
+ {% endif %}
+ {% endfor %}
+
+ {% endif %}
+ {% endfor %}
+
+ {% if hasChapterContent and chapter_next|trans != chapter_next %}
+
+ {% endif %}
+ {% endfor %}
+ {% endblock %}
+
+
+
+
+{% endblock %}
+
diff --git a/app/src/Ressources/translations/admin+intl-icu.en.yaml b/app/src/Ressources/translations/admin+intl-icu.en.yaml
new file mode 100644
index 0000000..aa51ca9
--- /dev/null
+++ b/app/src/Ressources/translations/admin+intl-icu.en.yaml
@@ -0,0 +1,85 @@
+admin:
+ dashboard:
+ menu:
+ label:
+ dashboard: Dashboard
+ administration: Administration
+ users: Users
+ user_roles: 'User Roles'
+ system: System
+ logs: Logs
+ phpinfo: phpinfo
+ my_profile: 'My Profile'
+ crud:
+ generic:
+ is_active: 'is active'
+ created_at: 'created at'
+ updated_at: 'updated at'
+ user:
+ titles:
+ index_page: Users
+ account_information: 'Account Information'
+ change_password: 'Change password'
+ admin_settings: 'Admin Settings'
+ user_settings: 'User Settings'
+ security: 'Security'
+ label:
+ username: Username
+ email: eMail
+ user_roles: 'User Roles'
+ fullname: 'Full name'
+ firstname: Firstname
+ lastname: Lastname
+ new_password: Password
+ new_password_repeat: 'Repeat password'
+ time_zone: Timezone
+ country: Country
+ mail_verified: 'Email is verfied'
+ user: User
+ date_format: 'Date format'
+ time_format: 'Time format'
+ locale: Language
+ messages:
+ passwort_not_match: 'Passwords dont match'
+ twofactor:
+ badge:
+ enabled: 'two-step verification is enabled'
+ disabled: 'two-step verification is not enabled'
+ label:
+ explain_paragraph: 'The two-step verification increases the security of your account. In addition to your password, a code is required in a second step, which is displayed on your smartphone.'
+ apps_intro: 'You may use one of the following apps for verification:'
+ link_enable: 'Click here to enable two-step verifcation'
+ link_disable_intro: 'If you want to disable the two-step verifcation:'
+ link_disable: 'Click here to disable'
+ step1: 'Step 1: scan this QR-Code'
+ step2: 'Step 2: verfication code'
+ submit_enable: 'Enable verification'
+ backup:
+ title: 'Backup Codes'
+ intro: 'Backup codes are one-time authentication codes, which can be used instead of the actual codes. They''re meant as emergency codes, when the authentication device is not available, and you have to pass the two-factor authentication process.'
+ messages:
+ verification_code_wrong: 'the verification code is wrong'
+
+ user_roles:
+ titles:
+ index_page: 'User Roles'
+ label:
+ new_role: 'New Role'
+ role: Role
+ name: Name
+ description: Description
+ parent_roles: 'Parent Roles'
+ logs:
+ titles:
+ index_page: Logs
+ general: General
+ request_information: 'Request Information'
+ label:
+ level: Level
+ context: Context
+ subcontext: Subcontext
+ message: Message
+ request_method: 'Request Method'
+ request_path: 'Request Path'
+ client_ip: 'Client IP'
+ client_locale: 'Client locale'
diff --git a/app/src/Ressources/translations/core+intl-icu.en.yaml b/app/src/Ressources/translations/core+intl-icu.en.yaml
new file mode 100644
index 0000000..bbc0a1d
--- /dev/null
+++ b/app/src/Ressources/translations/core+intl-icu.en.yaml
@@ -0,0 +1,97 @@
+service:
+ generic:
+ email:
+ greeting: Hey
+ farewell: Cheers
+ header: ' '
+ footer: 'This is the footer'
+ expire: 'This link will expire in {expire}.'
+ form:
+ submit:
+ label_back: Back to login
+ login:
+ page:
+ page_title: 'Acme Login'
+ submit_label: 'Log in'
+ reset_link: 'Reset password'
+ register_link: Register
+ form:
+ placeholder_email: Email
+ placeholder_username: Username
+ placeholder_both: Username or Email
+ twofactor:
+ page_title: 'Two Factor Authentication'
+ info_text: 'Open your Authenticator app and type in the number.'
+ registration:
+ email:
+ subject: 'Please confirm your email'
+ body_title: 'Please confirm your email'
+ body_text: 'Please confirm your email address by clicking the following link:'
+ button_text: 'Confirm my Email'
+ page:
+ page_title: Registration
+ header_text: 'Please fill out the following fields to register'
+ header_mailsent_text: 'We sent you a link to verify your email address. Open the email and click the link. if you did not recieved the mail. you can login to resend the mail.'
+ form:
+ username:
+ label: Username
+ placeholder: 'Your username'
+ messages:
+ blank: 'Please enter your username'
+ alphanummeric: 'Only letters and numbers are allowed'
+ email:
+ label: Email
+ placeholder: john@doe.com
+ messages:
+ blank: 'Please enter your email'
+ firstname:
+ label: Firstname
+ placeholder: Firstname
+ messages:
+ blank: 'Please enter your firstname'
+ lastname:
+ label: Lastname
+ placeholder: Lastname
+ messages:
+ blank: 'Please enter your lastname'
+ password:
+ label: Password
+ repeat_label: 'Repeat Password'
+ messages:
+ blank: 'Please enter a password'
+ no_match: 'The password fields must match.'
+ min_lenght: 'Your password should be at least {limit} characters'
+ terms:
+ label: 'Agree terms'
+ url: /en/terms
+ messages:
+ agree: 'You should agree to our terms.'
+ submit:
+ label: Register
+ verify_mail:
+ page:
+ title: 'Resend Email Verification'
+ text: 'Enter your email address and we will send you a link to verify your account'
+ submit_label: 'Send email'
+ message:
+ not_verified: Your mailadress is not verfied yet. Send mail again
+ password_reset:
+ email:
+ subject: 'Your password reset request'
+ body_title: 'Reset password'
+ body_text: 'To reset your password, please click button below'
+ button_text: 'Reset Password'
+ page:
+ title: 'Reset Password'
+ text: 'Enter your email address and we will send you a link to reset your password.'
+ submit_label: 'Send password reset email'
+ page_sent:
+ title: 'Password Reset Email Sent'
+ text_intro: 'If an account matching your email exists, then an email was just sent that contains a link that you can use to reset your password.'
+ text_outro: 'If you don''t receive an email please check your spam folder or try again .'
+ submit_label: 'Back to page'
+ page_reset:
+ title: 'Reset Password'
+ text: ''
+ submit_label: 'Reset password now'
+
diff --git a/app/src/Ressources/translations/messages+intl-icu.en.yaml b/app/src/Ressources/translations/messages+intl-icu.en.yaml
new file mode 100644
index 0000000..e69de29
diff --git a/app/src/Ressources/translations/tos+intl-icu.en.yaml b/app/src/Ressources/translations/tos+intl-icu.en.yaml
new file mode 100644
index 0000000..0af9fe0
--- /dev/null
+++ b/app/src/Ressources/translations/tos+intl-icu.en.yaml
@@ -0,0 +1,73 @@
+basics:
+ title:
+ page: 'Terms and Conditions of Use'
+ tos: 'Terms and Conditions of Use'
+
+chapter_1:
+ title: 'Chapter'
+ text: |
+ Lorem Ipsum
+ subchapter_1:
+ title: 'Subchapter'
+ text: |
+ Lorem Ipsum
+ subsubchapter_1:
+ title: 'SubSubChapter'
+ text: |
+ Lorem Ipsum
+ subsubchapter_2:
+ title: 'SubSubChapter'
+ text: |
+ Lorem Ipsum
+chapter_2:
+ title: 'Terms'
+ text: |
+ By accessing this Website, accessible from foo.com, you are agreeing to be bound by these Website Terms and Conditions of Use and agree that you are responsible for the agreement with any applicable local laws. If you disagree with any of these terms, you are prohibited from accessing this site. The materials contained in this Website are protected by copyright and trade mark law.
+
+chapter_3:
+ title: 'Use License'
+ text: |
+ Permission is granted to temporarily download one copy of the materials on AcmeCorps's Website for personal, non-commercial transitory viewing only. This is the grant of a license, not a transfer of title, and under this license you may not:
+
+ modify or copy the materials;
+ use the materials for any commercial purpose or for any public display;
+ attempt to reverse engineer any software contained on AcmeCorps's Website;
+ remove any copyright or other proprietary notations from the materials; or
+ transferring the materials to another person or "mirror" the materials on any other server.
+
+ This will let AcmeCorps to terminate upon violations of any of these restrictions. Upon termination, your viewing right will also be terminated and you should destroy any downloaded materials in your possession whether it is printed or electronic format. These Terms of Service has been created with the help of the Terms Of Service Generator .
+
+chapter_4:
+ title: 'Disclaimer'
+ text: |
+ All the materials on AcmeCorps’s Website are provided "as is". AcmeCorps makes no warranties, may it be expressed or implied, therefore negates all other warranties. Furthermore, AcmeCorps does not make any representations concerning the accuracy or reliability of the use of the materials on its Website or otherwise relating to such materials or any sites linked to this Website.
+
+chapter_5:
+ title: 'Limitations'
+ text: |
+ AcmeCorps or its suppliers will not be hold accountable for any damages that will arise with the use or inability to use the materials on AcmeCorps’s Website, even if AcmeCorps or an authorize representative of this Website has been notified, orally or written, of the possibility of such damage. Some jurisdiction does not allow limitations on implied warranties or limitations of liability for incidental damages, these limitations may not apply to you.
+
+chapter_6:
+ title: 'Limitations'
+ text: |
+ The materials appearing on AcmeCorps’s Website may include technical, typographical, or photographic errors. AcmeCorps will not promise that any of the materials in this Website are accurate, complete, or current. AcmeCorps may change the materials contained on its Website at any time without notice. AcmeCorps does not make any commitment to update the materials.
+
+chapter_7:
+ title: 'Links'
+ text: |
+ AcmeCorps has not reviewed all of the sites linked to its Website and is not responsible for the contents of any such linked site. The presence of any link does not imply endorsement by AcmeCorps of the site. The use of any linked website is at the user’s own risk.
+
+chapter_8:
+ title: 'Site Terms of Use Modifications'
+ text: |
+ AcmeCorps may revise these Terms of Use for its Website at any time without prior notice. By using this Website, you are agreeing to be bound by the current version of these Terms and Conditions of Use.
+
+chapter_9:
+ title: 'Your Privacy'
+ text: |
+ Please read our Privacy Policy.
+
+chapter_10:
+ title: 'Governing Law'
+ text: |
+ Any claim related to AcmeCorps's Website shall be governed by the laws of ch without regards to its conflict of law provisions.
\ No newline at end of file
diff --git a/app/src/Ressources/translations/validators+intl-icu.en.yaml b/app/src/Ressources/translations/validators+intl-icu.en.yaml
new file mode 100644
index 0000000..b016ddd
--- /dev/null
+++ b/app/src/Ressources/translations/validators+intl-icu.en.yaml
@@ -0,0 +1,9 @@
+validators:
+ general:
+ blank: 'This value cant be blank'
+ username:
+ alphanummeric: 'Only letters and numbers are allowed'
+ email:
+ invalid: 'The email is not a valid email.'
+
+
diff --git a/app/src/Security/ApiTokenAuthenticator.php b/app/src/Security/ApiTokenAuthenticator.php
new file mode 100644
index 0000000..99e3b41
--- /dev/null
+++ b/app/src/Security/ApiTokenAuthenticator.php
@@ -0,0 +1,63 @@
+userRepository = $userRepository;
+ }
+
+ public function supports(Request $request): ?bool
+ {
+ return $request->headers->has('x-api-token');
+ }
+
+ public function authenticate(Request $request): Passport
+ {
+ $apiToken = $request->headers->get('x-api-token');
+ if ($apiToken === null) {
+ throw new CustomUserMessageAuthenticationException('No API token provided');
+ }
+
+ return new SelfValidatingPassport(
+ new UserBadge(
+ $apiToken,
+ function ($apiToken) {
+ $user = $this->userRepository->findByApiToken($apiToken);
+
+ if (!$user) {
+ throw new UserNotFoundException();
+ }
+
+ return $user;
+ }
+ )
+ );
+ }
+
+ public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
+ {
+ return null;
+ }
+
+ public function onAuthenticationFailure(Request $request, AuthenticationException $exception): ?Response
+ {
+ $data = ['message'=> strstr($exception->getMessageKey(), implode(', ', $exception->getMessageData())) ];
+ return new JsonResponse($data, Response::HTTP_UNAUTHORIZED);
+ }
+}
diff --git a/app/src/Security/EmailVerifier.php b/app/src/Security/EmailVerifier.php
new file mode 100644
index 0000000..a0b8fa1
--- /dev/null
+++ b/app/src/Security/EmailVerifier.php
@@ -0,0 +1,58 @@
+verifyEmailHelper->generateSignature(
+ $verifyEmailRouteName,
+ $user->getId(),
+ $user->getEmail(),
+ ['id' => $user->getPid()]
+ );
+
+ $context = $email->getContext();
+ $context['signedUrl'] = $signatureComponents->getSignedUrl();
+ $context['greeetingName'] = (!empty($user->getFirstname()) ? $user->getFirstname() : $user->getUsername());
+ $context['expiresAtMessageKey'] = $signatureComponents->getExpirationMessageKey();
+ $context['expiresAtMessageData'] = $signatureComponents->getExpirationMessageData();
+
+ $email->context($context);
+
+ $this->mailer->sendMail($email, $user);
+ }
+
+ /**
+ * @throws VerifyEmailExceptionInterface
+ */
+ public function handleEmailConfirmation(Request $request, UserInterface $user): void
+ {
+ $this->verifyEmailHelper->validateEmailConfirmation($request->getUri(), $user->getId(), $user->getEmail());
+
+ $user->setIsVerified(true);
+
+ $this->entityManager->persist($user);
+ $this->entityManager->flush();
+
+ $this->log->user_emailverified($user);
+ }
+}
diff --git a/app/src/Security/LoginFormAuthenticator.php b/app/src/Security/LoginFormAuthenticator.php
new file mode 100644
index 0000000..c593675
--- /dev/null
+++ b/app/src/Security/LoginFormAuthenticator.php
@@ -0,0 +1,106 @@
+entityManager = $entityManager;
+ $this->log = $log;
+ }
+
+ public function authenticate(Request $request): Passport
+ {
+ $userIdentifier = $request->request->get('username', '');
+
+ $request->getSession()->set(Security::LAST_USERNAME, $userIdentifier);
+
+ return new Passport(
+ new UserBadge($userIdentifier, function ($userIdentifier) {
+ $user_username_exists = $this->entityManager->getRepository(User::class)->findOneBy(['username' => $userIdentifier]);
+ $user_email_exists = null;
+ $activeParams = null;
+
+ if (!$user_username_exists) {
+ $user_email_exists = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $userIdentifier]);
+
+ if (!$user_email_exists) {
+ $this->log->login('User could not be found.', false);
+ throw new UserNotFoundException();
+ } else {
+ $user_username_exists = null;
+ $activeParams['email'] = $userIdentifier;
+ }
+ } else {
+ $activeParams['username'] = $userIdentifier;
+ }
+
+ if ($activeParams != null) {
+ $activeParams['deletedAt'] = null;
+
+ $user = $this->entityManager->getRepository(User::class)->findOneBy($activeParams);
+
+ if (!$user) {
+ $this->log->login('User not found', false);
+ throw new UserNotFoundException();
+ } else {
+ if ($user->isActive() and $user->isVerified()) {
+ $this->log->login('', true);
+ return $user;
+ } elseif (!$user->isVerified() and $user->isActive()) {
+ $this->log->login('email not verified', false);
+ throw new CustomUserMessageAuthenticationException('login_email_not_verified');
+ } else {
+ $this->log->login('User not active', false);
+ throw new UserNotFoundException();
+ }
+ }
+ }
+ }),
+ new PasswordCredentials($request->request->get('password', '')),
+ [
+ new CsrfTokenBadge('authenticate', $request->request->get('_csrf_token')),
+ ]
+ );
+ }
+
+ public function onAuthenticationSuccess(Request $request, TokenInterface $token, string $firewallName): ?Response
+ {
+ if ($targetPath = $this->getTargetPath($request->getSession(), $firewallName)) {
+ return new RedirectResponse($targetPath);
+ }
+
+ return new RedirectResponse($this->urlGenerator->generate('admin'));
+ }
+
+ protected function getLoginUrl(Request $request): string
+ {
+ return $this->urlGenerator->generate(self::LOGIN_ROUTE);
+ }
+}
diff --git a/app/src/Service/Log/LogMailerService.php b/app/src/Service/Log/LogMailerService.php
new file mode 100644
index 0000000..85b200b
--- /dev/null
+++ b/app/src/Service/Log/LogMailerService.php
@@ -0,0 +1,129 @@
+generalLogging = $generalLogging;
+ $this->fullLogging = $fullLogging;
+ return $this;
+ }
+
+
+ public function sendMail($mail, ?String $message=null, ?bool $success=false, ?User $user = null): self
+ {
+ $message = $this->getMessagePropertyString($mail);
+
+ if ($user) {
+ $this->log->setUser($user);
+ }
+
+ if ($success) {
+ if ($this->generalLogging) {
+ $subContext = 'sent';
+ $this->info('mailer', $subContext, $message, $success);
+ }
+ } else {
+ $subContext = 'not sent';
+ $this->error('mailer', $subContext, $message, $success);
+ }
+
+ if ($this->generalLogging) {
+ $this->setSentMailDetailedLog($mail, $success, $user);
+ }
+ return $this;
+ }
+
+
+ private function getMessageProperties($message): ?array
+ {
+ $collection = [];
+ $collection['messageId'] = '';
+
+ if ($message instanceof SentMessage) {
+ $collection['from'] = $message->getEnvelope()->getSender()->getAddress();
+
+ $collection['to'] = join(
+ ', ',
+ array_map(function ($entry) {
+ return $entry->getAddress();
+ }, $message->getEnvelope()->getRecipients())
+ );
+
+ $collection['messageId'] = $message->getMessageId();
+ $collection['subject'] = $message->getOriginalMessage()->getHeaders()->get('Subject')->getValue();
+ $collection['bodyHTML'] = $message->getOriginalMessage()->getHtmlBody();
+ $collection['bodyText'] = $message->getOriginalMessage()->getTextBody();
+ } else {
+ $collection['from'] = join(
+ ', ',
+ array_map(function ($entry) {
+ return $entry->getAddress();
+ }, $message->getFrom())
+ );
+ $collection['to'] = join(
+ ', ',
+ array_map(function ($entry) {
+ return $entry->getAddress();
+ }, $message->getTo())
+ );
+ $collection['subject'] = $message->getSubject();
+ $collection['bodyHTML'] = $message->getHtmlBody();
+ $collection['bodyText'] = $message->getTextBody();
+ }
+ return array_filter($collection);
+ }
+
+ private function getMessagePropertyString($message, $withBody = false): ?String
+ {
+ $return = "";
+ foreach ($this->getMessageProperties($message) as $key => $value) {
+ if ($value) {
+ if (($withBody and str_contains($key, 'body')) or !str_contains($key, 'body')) {
+ $return .= strtoupper($key) . ': ' . $value . PHP_EOL;
+ }
+ }
+ }
+ return $return;
+ }
+
+ private function setSentMailDetailedLog($message, ?bool $success, ?User $user): void
+ {
+ $properties = $this->getMessageProperties($message);
+
+ $maillog = new EntityEmail();
+ $maillog->setSubject($properties['subject']);
+ $maillog->setSenderEmail($properties['from']);
+ $maillog->setRecieverEmail($properties['to']);
+
+ if ($this->fullLogging) {
+ $maillog->setHtml($properties['bodyHTML']);
+ $maillog->setText($properties['bodyText']);
+ }
+
+ if (isset($properties['messageId'])) {
+ $maillog->setMessageId($properties['messageId']);
+ }
+
+ if ($user) {
+ $maillog->setUser($user);
+ }
+ if (!$success) {
+ $maillog->setFailed(new \DateTime());
+ }
+
+ $this->manager->persist($maillog);
+ $this->manager->flush();
+ }
+}
diff --git a/app/src/Service/Log/LogService.php b/app/src/Service/Log/LogService.php
new file mode 100644
index 0000000..748ce49
--- /dev/null
+++ b/app/src/Service/Log/LogService.php
@@ -0,0 +1,73 @@
+manager = $manager;
+ $this->security = $security;
+ $this->requestStack = $requestStack;
+
+ $this->log = new Log();
+ $this->log->setUser($this->security->getUser()) ;
+
+ if (!empty($this->requestStack->getCurrentRequest())) {
+ $this->log->setClientIP($this->requestStack->getCurrentRequest()->getClientIp());
+ $this->log->setClientLocale($this->requestStack->getCurrentRequest()->getLocale());
+ $this->log->setRequestMethod($this->requestStack->getCurrentRequest()->getMethod());
+ $this->log->setRequestPath($this->requestStack->getCurrentRequest()->getPathInfo());
+ }
+ }
+
+ private function logEvent(?String $level, ?String $context, ?String $subcontext = null, ?String $message, ?bool $isSuccess = null): self
+ {
+ $this->log->setLevel($level);
+ $this->log->setContext($context);
+ $this->log->setSubcontext($subcontext);
+ $this->log->setMessage($message);
+ $this->log->setSuccess($isSuccess);
+
+ $this->manager->persist($this->log);
+ $this->manager->flush();
+ return $this;
+ }
+
+ public function info(?String $context, ?String $subcontext, ?String $message, ?bool $isSuccess = null): self
+ {
+ $this->logEvent('INFO', $context, $subcontext, $message, $isSuccess);
+ return $this;
+ }
+
+ public function debug(?String $context, ?String $subcontext, ?String $message, ?bool $isSuccess = null): self
+ {
+ $this->logEvent('DEBUG', $context, $subcontext, $message, $isSuccess);
+ return $this;
+ }
+
+ public function warning(?String $context, ?String $subcontext, ?String $message, ?bool $isSuccess = false): self
+ {
+ $this->logEvent('WARNING', $context, $subcontext, $message, $isSuccess);
+ return $this;
+ }
+
+ public function error(?String $context, ?String $subcontext, ?String $message, ?bool $isSuccess = false): self
+ {
+ $this->logEvent('ERROR', $context, $subcontext, $message, $isSuccess);
+ return $this;
+ }
+}
diff --git a/app/src/Service/Log/LogSystemService.php b/app/src/Service/Log/LogSystemService.php
new file mode 100644
index 0000000..b780b12
--- /dev/null
+++ b/app/src/Service/Log/LogSystemService.php
@@ -0,0 +1,14 @@
+debug('fixtures', $subContext, $message, $success);
+ return $this;
+ }
+}
diff --git a/app/src/Service/Log/LogUserService.php b/app/src/Service/Log/LogUserService.php
new file mode 100644
index 0000000..8cd5f02
--- /dev/null
+++ b/app/src/Service/Log/LogUserService.php
@@ -0,0 +1,61 @@
+requestStack->getCurrentRequest()->get('email');
+ $userByEmail = $this->manager->getRepository(User::class)->findOneBy(['email' => $userIdentifier]);
+
+ if ($userByEmail == null) {
+ $userByUsername = $this->manager->getRepository(User::class)->findOneBy(['username' => $userIdentifier]);
+ $user = $userByUsername;
+ } else {
+ $user = $userByEmail;
+ }
+ if ($user) {
+ $this->log->setUser($user);
+ }
+
+ if ($success) {
+ $this->info('user', 'login', $userIdentifier.' login successful '.$message, true);
+ } else {
+ $this->info('user', 'login', $userIdentifier.' failed logging in '.$message, false);
+ }
+ return $this;
+ }
+
+ public function logout($user): self
+ {
+ $this->log->setUser($user);
+ $this->info('user', 'logout', $user.' logged out ', true);
+ return $this;
+ }
+
+ public function user_created($user): self
+ {
+ $this->log->setUser($user);
+ $this->info('user', 'created', $user.' created', true);
+ return $this;
+ }
+
+ public function user_emailverified($user): self
+ {
+ $this->log->setUser($user);
+ $this->info('user', 'email verified', $user->getEmail().' verifed', true);
+ return $this;
+ }
+
+
+ public function passwordResetMail($success=false): self
+ {
+ $this->info('user', 'sent password reset mail', $this->log->getUser().' logged out ', true);
+ return $this;
+ }
+}
diff --git a/app/src/Service/MailSender.php b/app/src/Service/MailSender.php
new file mode 100644
index 0000000..9d1762a
--- /dev/null
+++ b/app/src/Service/MailSender.php
@@ -0,0 +1,51 @@
+log = $log;
+ $params = $params->get('app')['mailer'];
+ $this->log->setMailLogging($params['logging_general'], $params['logging_full']);
+
+ $this->fromMail = $params['from_email'];
+ $this->fromName = $params['from_name'];
+ $this->textMailLinkFomat = $params['text_body']['link_format'] ?? 'table';
+ $this->textMailWidth = $params['text_body']['width'] ?? '70';
+ }
+
+ public function sendMail(Email $message, ?User $user = null): void
+ {
+ //If template is used, render it
+ if (!empty($message->getHtmlTemplate())) {
+ $renderedHtmlBody = $this->twig->render($message->getHtmlTemplate(), $message->getContext());
+ $message->html($renderedHtmlBody);
+ $textContent = new Html2Text($renderedHtmlBody, ['do_links' => $this->textMailLinkFomat, 'width' => $this->textMailWidth]);
+ $message->text(trim($textContent->getText()));
+ }
+
+ $message->from($this->fromName == null ? new Address($this->fromMail) : new Address($this->fromMail, $this->fromName));
+ try {
+ $message = $this->mailer->send($message);
+ $this->log->sendMail($message, null, true, $user);
+ } catch (TransportExceptionInterface $e) {
+ $this->log->sendMail($message, $e->getMessage(), false, $user);
+ }
+ }
+}
diff --git a/app/src/Service/Utils/ConfigParamsTestHelper.php b/app/src/Service/Utils/ConfigParamsTestHelper.php
new file mode 100644
index 0000000..6fe0211
--- /dev/null
+++ b/app/src/Service/Utils/ConfigParamsTestHelper.php
@@ -0,0 +1,224 @@
+separator = $separator;
+ $this->pathPrefix = $pathPrefix;
+
+ $this->projectDir = Path::canonicalize(__DIR__.'/../../..');
+ $this->configDir = Path::canonicalize($this->projectDir.$configDirectory);
+ $this->configDirTest = Path::canonicalize($this->projectDir.$configDirectory.'/test');
+ $this->paramFileOrginal = Path::canonicalize($this->configDir .'/'.$paramsFile);
+ $this->paramFileTest = Path::canonicalize($this->configDirTest .'/'.$paramsFile);
+
+ $this->setup();
+ }
+
+ /**
+ * setup
+ *
+ * @return bool
+ */
+ public function setup(): ?bool
+ {
+ if (!$this->isParamFileOrginalExisting()) {
+ return false;
+ }
+ $this->copyParamFileTest();
+ $this->loadTestFile();
+
+ return null;
+ }
+
+ /**
+ * isParamFileOrginalExisting checks wether /config/packages/parameters.yaml exists.
+ * paramsFile can be changed when creating
+ *
+ * @return bool
+ */
+ public function isParamFileOrginalExisting(): bool
+ {
+ $fs = new Filesystem();
+ if ($fs->exists($this->configDir)) {
+ if ($fs->exists($this->paramFileOrginal)) {
+ return true;
+ } else {
+ throw new \ErrorException('Config file not found:'. $this->paramFileOrginal.'.');
+ }
+ } else {
+ throw new \ErrorException('configDirectory not found:'. $this->configDir.'.');
+ }
+ return false;
+ }
+
+
+ /**
+ * refreshParamFileTest
+ * copies orignal param file to test file
+ * @return void
+ */
+ public function copyParamFileTest(): void
+ {
+ $fs = new Filesystem();
+ $fs->remove($this->paramFileTest);
+ $fs->copy($this->paramFileOrginal, $this->paramFileTest, true);
+ $this->loadTestFile();
+ }
+
+
+ public function loadTestFile(): void
+ {
+ $this->yamlTest = Yaml::parseFile($this->paramFileTest);
+ $this->yamlTestPaths = $this->generateYamlPaths($this->yamlTest);
+ }
+
+ /**
+ * generateYamlPaths
+ * generate path, and sanetize it
+ * @param mixed $tree
+ * @return array
+ */
+ protected function generateYamlPaths($tree): ?array
+ {
+ $paths = $this->generateYamlPathsTree($tree);
+
+ array_walk($paths, array($this, 'cleanupGeneratedYamlSubstringPaths'), $paths);
+ $paths = array_filter($paths);
+
+ $pattern = '/'.preg_quote(self::END_OF_PATH_DISTINCT_VALUE, '/').'$/';
+ $paths = preg_replace($pattern, '', $paths);
+ return $paths;
+ }
+
+ /**
+ * generateYamlPathsTree
+ * recoursive loop thru fields to greate paths
+ * @param mixed $tree nested arrray below
+ * @param mixed $parent parent above
+ * @return Array
+ */
+ protected function generateYamlPathsTree($tree, $parent=null): array
+ {
+ $paths = array();
+
+ if ($parent !== null) {
+ $parent = $parent.$this->separator;
+ }
+ foreach ($tree as $k => $v) {
+ if (is_array($v)) {
+ $currentPath = $parent.$k;
+ $paths[] = $currentPath;
+ $paths = array_merge($paths, $this->generateYamlPathsTree($v, $currentPath));
+ } else {
+ $paths[] = $parent.$k.self::END_OF_PATH_DISTINCT_VALUE;
+ }
+ }
+ return $paths;
+ }
+
+ /**
+ * cleanupGeneratedYamlSubstringPaths
+ * keep only last element of chain.
+ * e.g will be kept 'house.room.table', but 'house'|'house.room' will be purged
+ *
+ * @param mixed $value
+ * @param mixed $key
+ * @param mixed $paths
+ * @return void
+ */
+ protected function cleanupGeneratedYamlSubstringPaths(&$value, $key, $paths)
+ {
+ unset($paths[$key]);
+ if ($this->substring_in_array($value, $paths)) {
+ $value = null;
+ }
+ }
+
+ /**
+ * substring_in_array
+ * is substring in found in array values
+ *
+ * @param mixed $needle
+ * @param mixed $haystack
+ * @return bool
+ */
+ protected function substring_in_array($needle, $haystack): bool
+ {
+ $found_keys=[];
+ foreach ($haystack as $key => $value) {
+ if (false !== strpos($value, $needle)) {
+ $found_keys[] = $key;
+ }
+ }
+ return !empty($found_keys);
+ }
+
+ /**
+ * updateValue
+ * update yml by path and save it.
+ *
+ * @param mixed $path
+ * @param mixed $value
+ * @return bool
+ */
+ public function updateValue(string $path, $value): bool
+ {
+ $pathPrefix = (!empty($this->pathPrefix)) ? $this->pathPrefix.$this->separator : '';
+
+
+ if (!in_array($pathPrefix.$path, $this->yamlTestPaths)) {
+ $pattern = '/^'.preg_quote($pathPrefix, '.').'/';
+ $allowedPathsNoPrefix =preg_replace($pattern, '', $this->yamlTestPaths);
+ $allowedPaths = join(PHP_EOL, $allowedPathsNoPrefix);
+ throw new \LogicException('path not found: '. $path.PHP_EOL.'Allowed paths are:'.PHP_EOL.$allowedPaths);
+ }
+
+ $pathParts = explode($this->separator, $path);
+ $yaml = &$this->yamlTest;
+
+ foreach ($pathParts as $part) {
+ $yaml = &$yaml[$part];
+ }
+ $yaml = $value;
+ return $this->saveTestFile();
+ }
+
+ /**
+ * saveTestFile
+ *
+ * @return bool
+ */
+ public function saveTestFile(): bool
+ {
+ if (!empty($this->yamlTest)) {
+ $yaml = Yaml::dump($this->yamlTest, 10, 4);
+ return file_put_contents($this->paramFileTest, $yaml.PHP_EOL.PHP_EOL) != false;
+ }
+ return false;
+ }
+}
diff --git a/app/symfony.lock b/app/symfony.lock
new file mode 100644
index 0000000..ab456f1
--- /dev/null
+++ b/app/symfony.lock
@@ -0,0 +1,378 @@
+{
+ "api-platform/core": {
+ "version": "2.6",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "2.5",
+ "ref": "05b57782a78c21a664a42055dc11cf1954ca36bb"
+ },
+ "files": [
+ "config/packages/api_platform.yaml",
+ "config/routes/api_platform.yaml",
+ "src/Entity/.gitignore"
+ ]
+ },
+ "doctrine/annotations": {
+ "version": "1.13",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "1.10",
+ "ref": "64d8583af5ea57b7afa4aba4b159907f3a148b05"
+ }
+ },
+ "doctrine/doctrine-bundle": {
+ "version": "2.7",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "2.4",
+ "ref": "ddddd8249dd55bbda16fa7a45bb7499ef6f8e90e"
+ },
+ "files": [
+ "config/packages/doctrine.yaml",
+ "src/Entity/.gitignore",
+ "src/Repository/.gitignore"
+ ]
+ },
+ "doctrine/doctrine-fixtures-bundle": {
+ "version": "3.4",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "3.0",
+ "ref": "1f5514cfa15b947298df4d771e694e578d4c204d"
+ },
+ "files": [
+ "src/DataFixtures/AppFixtures.php"
+ ]
+ },
+ "doctrine/doctrine-migrations-bundle": {
+ "version": "3.2",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "3.1",
+ "ref": "ee609429c9ee23e22d6fa5728211768f51ed2818"
+ },
+ "files": [
+ "config/packages/doctrine_migrations.yaml",
+ "migrations/.gitignore"
+ ]
+ },
+ "easycorp/easyadmin-bundle": {
+ "version": "4.3",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "3.0",
+ "ref": "b131e6cbfe1b898a508987851963fff485986285"
+ }
+ },
+ "nelmio/cors-bundle": {
+ "version": "2.2",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "1.5",
+ "ref": "6bea22e6c564fba3a1391615cada1437d0bde39c"
+ },
+ "files": [
+ "config/packages/nelmio_cors.yaml"
+ ]
+ },
+ "phpunit/phpunit": {
+ "version": "9.5",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "9.3",
+ "ref": "a6249a6c4392e9169b87abf93225f7f9f59025e6"
+ },
+ "files": [
+ ".env.test",
+ "phpunit.xml.dist",
+ "tests/bootstrap.php"
+ ]
+ },
+ "scheb/2fa-bundle": {
+ "version": "6.3",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "6.0",
+ "ref": "1e6f68089146853a790b5da9946fc5974f6fcd49"
+ },
+ "files": [
+ "config/packages/scheb_2fa.yaml",
+ "config/routes/scheb_2fa.yaml"
+ ]
+ },
+ "sensio/framework-extra-bundle": {
+ "version": "6.2",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "5.2",
+ "ref": "fb7e19da7f013d0d422fa9bce16f5c510e27609b"
+ },
+ "files": [
+ "config/packages/sensio_framework_extra.yaml"
+ ]
+ },
+ "stof/doctrine-extensions-bundle": {
+ "version": "1.7",
+ "recipe": {
+ "repo": "github.com/symfony/recipes-contrib",
+ "branch": "main",
+ "version": "1.2",
+ "ref": "e805aba9eff5372e2d149a9ff56566769e22819d"
+ }
+ },
+ "symfony/console": {
+ "version": "6.1",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "5.3",
+ "ref": "da0c8be8157600ad34f10ff0c9cc91232522e047"
+ },
+ "files": [
+ "bin/console"
+ ]
+ },
+ "symfony/debug-bundle": {
+ "version": "6.1",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "5.3",
+ "ref": "5aa8aa48234c8eb6dbdd7b3cd5d791485d2cec4b"
+ },
+ "files": [
+ "config/packages/debug.yaml"
+ ]
+ },
+ "symfony/flex": {
+ "version": "2.2",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "1.0",
+ "ref": "146251ae39e06a95be0fe3d13c807bcf3938b172"
+ },
+ "files": [
+ ".env"
+ ]
+ },
+ "symfony/framework-bundle": {
+ "version": "6.1",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "5.4",
+ "ref": "3cd216a4d007b78d8554d44a5b1c0a446dab24fb"
+ },
+ "files": [
+ "config/packages/cache.yaml",
+ "config/packages/framework.yaml",
+ "config/preload.php",
+ "config/routes/framework.yaml",
+ "config/services.yaml",
+ "public/index.php",
+ "src/Controller/.gitignore",
+ "src/Kernel.php"
+ ]
+ },
+ "symfony/lock": {
+ "version": "6.1",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "5.2",
+ "ref": "a1c8800e40ae735206bb14586fdd6c4630a51b8d"
+ },
+ "files": [
+ "config/packages/lock.yaml"
+ ]
+ },
+ "symfony/mailer": {
+ "version": "6.1",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "4.3",
+ "ref": "97a61eabb351d7f6cb7702039bcfe07fe9d7e03c"
+ },
+ "files": [
+ "config/packages/mailer.yaml"
+ ]
+ },
+ "symfony/maker-bundle": {
+ "version": "1.45",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "1.0",
+ "ref": "fadbfe33303a76e25cb63401050439aa9b1a9c7f"
+ }
+ },
+ "symfony/messenger": {
+ "version": "6.1",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "6.0",
+ "ref": "2523f7d31488903e247a522e760dc279be7f7aaf"
+ },
+ "files": [
+ "config/packages/messenger.yaml"
+ ]
+ },
+ "symfony/monolog-bundle": {
+ "version": "3.8",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "3.7",
+ "ref": "213676c4ec929f046dfde5ea8e97625b81bc0578"
+ },
+ "files": [
+ "config/packages/monolog.yaml"
+ ]
+ },
+ "symfony/notifier": {
+ "version": "6.1",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "5.0",
+ "ref": "c31585e252b32fe0e1f30b1f256af553f4a06eb9"
+ },
+ "files": [
+ "config/packages/notifier.yaml"
+ ]
+ },
+ "symfony/phpunit-bridge": {
+ "version": "6.1",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "5.3",
+ "ref": "97cb3dc7b0f39c7cfc4b7553504c9d7b7795de96"
+ },
+ "files": [
+ ".env.test",
+ "bin/phpunit",
+ "phpunit.xml.dist",
+ "tests/bootstrap.php"
+ ]
+ },
+ "symfony/routing": {
+ "version": "6.1",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "6.1",
+ "ref": "a44010c0d06989bd4f154aa07d2542d47caf5b83"
+ },
+ "files": [
+ "config/packages/routing.yaml",
+ "config/routes.yaml"
+ ]
+ },
+ "symfony/security-bundle": {
+ "version": "6.1",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "6.0",
+ "ref": "8a5b112826f7d3d5b07027f93786ae11a1c7de48"
+ },
+ "files": [
+ "config/packages/security.yaml"
+ ]
+ },
+ "symfony/translation": {
+ "version": "6.1",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "5.3",
+ "ref": "da64f5a2b6d96f5dc24914517c0350a5f91dee43"
+ },
+ "files": [
+ "config/packages/translation.yaml",
+ "translations/.gitignore"
+ ]
+ },
+ "symfony/twig-bundle": {
+ "version": "6.1",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "5.4",
+ "ref": "bb2178c57eee79e6be0b297aa96fc0c0def81387"
+ },
+ "files": [
+ "config/packages/twig.yaml",
+ "templates/base.html.twig"
+ ]
+ },
+ "symfony/validator": {
+ "version": "6.1",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "5.3",
+ "ref": "c32cfd98f714894c4f128bb99aa2530c1227603c"
+ },
+ "files": [
+ "config/packages/validator.yaml"
+ ]
+ },
+ "symfony/web-profiler-bundle": {
+ "version": "6.1",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "6.1",
+ "ref": "e42b3f0177df239add25373083a564e5ead4e13a"
+ },
+ "files": [
+ "config/packages/web_profiler.yaml",
+ "config/routes/web_profiler.yaml"
+ ]
+ },
+ "symfony/webapp-pack": {
+ "version": "1.1",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "1.0",
+ "ref": "2fb3513dbc139884fc5b7c751242b66f9f10f0c3"
+ },
+ "files": [
+ "config/packages/messenger.yaml"
+ ]
+ },
+ "symfonycasts/reset-password-bundle": {
+ "version": "1.14",
+ "recipe": {
+ "repo": "github.com/symfony/recipes",
+ "branch": "main",
+ "version": "1.0",
+ "ref": "97c1627c0384534997ae1047b93be517ca16de43"
+ },
+ "files": [
+ "config/packages/reset_password.yaml"
+ ]
+ },
+ "symfonycasts/verify-email-bundle": {
+ "version": "v1.11.0"
+ },
+ "twig/extra-bundle": {
+ "version": "v3.4.0"
+ }
+}
diff --git a/app/tests/Core/AuthTest.php b/app/tests/Core/AuthTest.php
new file mode 100644
index 0000000..f99b872
--- /dev/null
+++ b/app/tests/Core/AuthTest.php
@@ -0,0 +1,111 @@
+get(UserRepository::class);
+ $user = $userRepository->findOneByUsername('admin');
+ $client->loginUser($user);
+ return $client;
+ }
+
+
+ public function testOpenAdminAsAnonymous(): void
+ {
+ $client = static::createClient();
+ $client->request('GET', '/admin');
+ $this->assertResponseRedirects('/en/login');
+ }
+
+ public function testOpenAdminAsAdminUser(): void
+ {
+
+ $client = $this->clientLoginAsAdmin();
+ $client->request('GET', '/admin');
+ $this->assertResponseIsSuccessful();
+ $this->assertResponseStatusCodeSame(200);
+ }
+
+ public function testLoginRedirect(): void
+ {
+ $client = static::createClient();
+ $client->request('GET', '/login');
+ $this->assertResponseRedirects('/en/login');
+ }
+
+ public function testUserAccessingAuthPages(): void
+ {
+ $pages = ['/en/login','/register','/reset-password'];
+ $client = $this->clientLoginAsAdmin();
+ foreach ($pages as $page)
+ {
+ $client->request('GET', $page);
+ $this->assertResponseRedirects();
+ $this->assertTrue($client->getResponse()->isRedirect('/authbridge'), $page);
+ }
+
+ }
+
+ public function testAnonymousAccessingAuthPages(): void
+ {
+ $pages = ['/en/login','/register','/reset-password'];
+ $client = static::createClient();
+ foreach ($pages as $page)
+ {
+ $client->request('GET', $page);
+ $assert = $client->getResponse()->isRedirect('/authbridge') == false ;
+ $this->assertTrue($assert, $page);
+ }
+ }
+
+ public function testLogout(): void
+ {
+ $client = $this->clientLoginAsAdmin();
+ $client->request('GET', '/logout');
+ $this->assertTrue($client->getResponse()->isRedirect());
+ }
+
+ /**
+ * @testdox Error 404
+ */
+ public function test404Page(): void
+ {
+ $client = static::createClient();
+ $client->request('GET', '/foobarXCFASDFGKPKéSFD');
+ $this->assertResponseStatusCodeSame(404);
+ }
+
+
+ public function testParamRegistrationActive(): void
+ {
+ $client = static::createClient();
+ $client->request('GET', '/register');
+ $this->assertResponseIsSuccessful();
+ $this->assertSelectorTextContains('h3', 'Registration');
+ $this->assertResponseStatusCodeSame(Response::HTTP_OK, $client->getResponse()->getStatusCode());
+ }
+
+ public function testParamPasswortResetActive(): void
+ {
+ $client = static::createClient();
+ $client->request('GET', '/reset-password');
+ $this->assertResponseIsSuccessful();
+ $this->assertSelectorTextContains('h3', 'Reset Password');
+ $this->assertResponseStatusCodeSame(Response::HTTP_OK, $client->getResponse()->getStatusCode());
+ }
+}
diff --git a/app/tests/bootstrap.php b/app/tests/bootstrap.php
new file mode 100644
index 0000000..2e56d9a
--- /dev/null
+++ b/app/tests/bootstrap.php
@@ -0,0 +1,11 @@
+bootEnv(dirname(__DIR__).'/.env.test');
+}
\ No newline at end of file
diff --git a/docker-compose.yml b/docker-compose.yml
new file mode 100644
index 0000000..277d494
--- /dev/null
+++ b/docker-compose.yml
@@ -0,0 +1,46 @@
+version: "3.8"
+services:
+
+ # PHP service
+ app:
+ container_name: symfony6app-php
+ build: ./docker/php/.
+ working_dir: /var/www/site
+ volumes:
+ - ./app:/var/www/site
+ networks:
+ - app-network
+
+ # Nginx service
+ nginx:
+ container_name: symfony6app-nginx
+ image: nginx:alpine
+ working_dir: /var/www/site
+ ports:
+ - 8001:80
+ volumes:
+ - ./app:/var/www/site
+ - ./docker/nginx/conf.d/:/etc/nginx/conf.d/
+ networks:
+ - app-network
+
+ # Mysql service
+ # change databasename in /docker/mysql/init.sql
+ mysql:
+ container_name: symfony6app-mysql
+ image: mysql:8.0
+ command: --default-authentication-plugin=mysql_native_password --init-file="/tmp/mysql/init.sql"
+ restart: always
+ environment:
+ - MYSQL_ROOT_PASSWORD=P@ssw0rd
+ volumes:
+ - ./docker/mysql/my.cnf:/etc/mysql/conf.d/my.cnf
+ - ./docker/mysql:/tmp/mysql
+ ports:
+ - 8989:3306
+ networks:
+ - app-network
+
+networks:
+ app-network:
+ driver: bridge
\ No newline at end of file
diff --git a/docker/mysql/init.sql b/docker/mysql/init.sql
new file mode 100644
index 0000000..e763d6b
--- /dev/null
+++ b/docker/mysql/init.sql
@@ -0,0 +1,3 @@
+CREATE DATABASE IF NOT EXISTS `default_app` DEFAULT CHARACTER SET = `utf8mb4`;
+CREATE DATABASE IF NOT EXISTS `default_test` DEFAULT CHARACTER SET = `utf8mb4`;
+USE `default_app`;
\ No newline at end of file
diff --git a/docker/mysql/my.cnf b/docker/mysql/my.cnf
new file mode 100644
index 0000000..c506c08
--- /dev/null
+++ b/docker/mysql/my.cnf
@@ -0,0 +1,10 @@
+[client]
+default-character-set=utf8mb4
+
+[mysql]
+default-character-set=utf8mb4
+
+[mysqld]
+collation-server = utf8mb4_general_ci
+init-connect='SET NAMES utf8mb4'
+character-set-server = utf8mb4
\ No newline at end of file
diff --git a/docker/nginx/conf.d/app.conf b/docker/nginx/conf.d/app.conf
new file mode 100644
index 0000000..4d1d18a
--- /dev/null
+++ b/docker/nginx/conf.d/app.conf
@@ -0,0 +1,25 @@
+server {
+ listen 80;
+ index index.php index.html;
+ error_log /var/log/nginx/app.error.log;
+ access_log /var/log/nginx/app.access.log;
+
+ root /var/www/site/public;
+
+ location ~ \.php$ {
+ fastcgi_buffer_size 32k;
+ fastcgi_buffers 4 32k;
+ try_files $uri =404;
+ fastcgi_split_path_info ^(.+\.php)(/.+)$;
+ fastcgi_pass app:9000;
+ fastcgi_index index.php;
+ include fastcgi_params;
+ fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
+ fastcgi_param PATH_INFO $fastcgi_path_info;
+ }
+
+ location / {
+ try_files $uri $uri/ /index.php?$query_string;
+ gzip_static on;
+ }
+}
\ No newline at end of file
diff --git a/docker/php/Dockerfile b/docker/php/Dockerfile
new file mode 100644
index 0000000..23e93f4
--- /dev/null
+++ b/docker/php/Dockerfile
@@ -0,0 +1,29 @@
+FROM php:8.1.11-fpm
+
+RUN apt-get update && apt-get install -y git #In this place you can specify all the extensions you need.
+RUN docker-php-ext-install pdo_mysql
+
+RUN apt-get update && apt-get install -y \
+ zlib1g-dev libicu-dev g++ \
+ libjpeg62-turbo-dev \
+ libzip-dev \
+ libpng-dev \
+ libwebp-dev \
+ libfreetype6-dev \
+ libxml2-dev \
+ git \
+ zip \
+ unzip \
+ && docker-php-ext-install pdo_mysql \
+ && docker-php-ext-configure gd --with-webp=/usr/include/webp --with-jpeg=/usr/include --with-freetype=/usr/include/freetype2/ \
+ && docker-php-ext-install -j$(nproc) gd \
+ && docker-php-ext-install -j$(nproc) zip \
+ && docker-php-ext-configure intl \
+ && docker-php-ext-install intl \
+ && docker-php-ext-install opcache
+
+
+COPY --from=composer:2.3.10 /usr/bin/composer /usr/bin/composer
+
+
+WORKDIR /var/www
\ No newline at end of file
diff --git a/docker/php/php.ini b/docker/php/php.ini
new file mode 100644
index 0000000..acb6901
--- /dev/null
+++ b/docker/php/php.ini
@@ -0,0 +1,10 @@
+memory_limit = 2G
+display_errors = 1
+error_reporting = -1
+date.timezone = UTC
+
+; Optimizations for Symfony, as documented on http://symfony.com/doc/current/performance.html
+opcache.memory_consumption = 256
+opcache.max_accelerated_files = 20000
+realpath_cache_size = 4096K
+realpath_cache_ttl = 600
\ No newline at end of file
diff --git a/symfony6-stack.code-workspace b/symfony6-stack.code-workspace
new file mode 100644
index 0000000..876a149
--- /dev/null
+++ b/symfony6-stack.code-workspace
@@ -0,0 +1,8 @@
+{
+ "folders": [
+ {
+ "path": "."
+ }
+ ],
+ "settings": {}
+}
\ No newline at end of file
diff --git a/tools/php-cs-fixer/composer.json b/tools/php-cs-fixer/composer.json
new file mode 100644
index 0000000..9558a4c
--- /dev/null
+++ b/tools/php-cs-fixer/composer.json
@@ -0,0 +1,5 @@
+{
+ "require": {
+ "friendsofphp/php-cs-fixer": "^3.9"
+ }
+}
diff --git a/tools/php-cs-fixer/composer.lock b/tools/php-cs-fixer/composer.lock
new file mode 100644
index 0000000..a1a2c57
--- /dev/null
+++ b/tools/php-cs-fixer/composer.lock
@@ -0,0 +1,2033 @@
+{
+ "_readme": [
+ "This file locks the dependencies of your project to a known state",
+ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
+ "This file is @generated automatically"
+ ],
+ "content-hash": "d6259871d743287ae7052fe34780cc5a",
+ "packages": [
+ {
+ "name": "composer/pcre",
+ "version": "3.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/pcre.git",
+ "reference": "e300eb6c535192decd27a85bc72a9290f0d6b3bd"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/pcre/zipball/e300eb6c535192decd27a85bc72a9290f0d6b3bd",
+ "reference": "e300eb6c535192decd27a85bc72a9290f0d6b3bd",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.4 || ^8.0"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^1.3",
+ "phpstan/phpstan-strict-rules": "^1.1",
+ "symfony/phpunit-bridge": "^5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Composer\\Pcre\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "http://seld.be"
+ }
+ ],
+ "description": "PCRE wrapping library that offers type-safe preg_* replacements.",
+ "keywords": [
+ "PCRE",
+ "preg",
+ "regex",
+ "regular expression"
+ ],
+ "support": {
+ "issues": "https://github.com/composer/pcre/issues",
+ "source": "https://github.com/composer/pcre/tree/3.0.0"
+ },
+ "funding": [
+ {
+ "url": "https://packagist.com",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/composer",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/composer/composer",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-02-25T20:21:48+00:00"
+ },
+ {
+ "name": "composer/semver",
+ "version": "3.3.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/semver.git",
+ "reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/semver/zipball/3953f23262f2bff1919fc82183ad9acb13ff62c9",
+ "reference": "3953f23262f2bff1919fc82183ad9acb13ff62c9",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.3.2 || ^7.0 || ^8.0"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^1.4",
+ "symfony/phpunit-bridge": "^4.2 || ^5"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Composer\\Semver\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nils Adermann",
+ "email": "naderman@naderman.de",
+ "homepage": "http://www.naderman.de"
+ },
+ {
+ "name": "Jordi Boggiano",
+ "email": "j.boggiano@seld.be",
+ "homepage": "http://seld.be"
+ },
+ {
+ "name": "Rob Bast",
+ "email": "rob.bast@gmail.com",
+ "homepage": "http://robbast.nl"
+ }
+ ],
+ "description": "Semver library that offers utilities, version constraint parsing and validation.",
+ "keywords": [
+ "semantic",
+ "semver",
+ "validation",
+ "versioning"
+ ],
+ "support": {
+ "irc": "irc://irc.freenode.org/composer",
+ "issues": "https://github.com/composer/semver/issues",
+ "source": "https://github.com/composer/semver/tree/3.3.2"
+ },
+ "funding": [
+ {
+ "url": "https://packagist.com",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/composer",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/composer/composer",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-04-01T19:23:25+00:00"
+ },
+ {
+ "name": "composer/xdebug-handler",
+ "version": "3.0.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/composer/xdebug-handler.git",
+ "reference": "ced299686f41dce890debac69273b47ffe98a40c"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/composer/xdebug-handler/zipball/ced299686f41dce890debac69273b47ffe98a40c",
+ "reference": "ced299686f41dce890debac69273b47ffe98a40c",
+ "shasum": ""
+ },
+ "require": {
+ "composer/pcre": "^1 || ^2 || ^3",
+ "php": "^7.2.5 || ^8.0",
+ "psr/log": "^1 || ^2 || ^3"
+ },
+ "require-dev": {
+ "phpstan/phpstan": "^1.0",
+ "phpstan/phpstan-strict-rules": "^1.1",
+ "symfony/phpunit-bridge": "^6.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Composer\\XdebugHandler\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "John Stevenson",
+ "email": "john-stevenson@blueyonder.co.uk"
+ }
+ ],
+ "description": "Restarts a process without Xdebug.",
+ "keywords": [
+ "Xdebug",
+ "performance"
+ ],
+ "support": {
+ "irc": "irc://irc.freenode.org/composer",
+ "issues": "https://github.com/composer/xdebug-handler/issues",
+ "source": "https://github.com/composer/xdebug-handler/tree/3.0.3"
+ },
+ "funding": [
+ {
+ "url": "https://packagist.com",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/composer",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/composer/composer",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-02-25T21:32:43+00:00"
+ },
+ {
+ "name": "doctrine/annotations",
+ "version": "1.13.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/annotations.git",
+ "reference": "648b0343343565c4a056bfc8392201385e8d89f0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/annotations/zipball/648b0343343565c4a056bfc8392201385e8d89f0",
+ "reference": "648b0343343565c4a056bfc8392201385e8d89f0",
+ "shasum": ""
+ },
+ "require": {
+ "doctrine/lexer": "1.*",
+ "ext-tokenizer": "*",
+ "php": "^7.1 || ^8.0",
+ "psr/cache": "^1 || ^2 || ^3"
+ },
+ "require-dev": {
+ "doctrine/cache": "^1.11 || ^2.0",
+ "doctrine/coding-standard": "^6.0 || ^8.1",
+ "phpstan/phpstan": "^1.4.10 || ^1.8.0",
+ "phpunit/phpunit": "^7.5 || ^8.0 || ^9.1.5",
+ "symfony/cache": "^4.4 || ^5.2",
+ "vimeo/psalm": "^4.10"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Common\\Annotations\\": "lib/Doctrine/Common/Annotations"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Guilherme Blanco",
+ "email": "guilhermeblanco@gmail.com"
+ },
+ {
+ "name": "Roman Borschel",
+ "email": "roman@code-factory.org"
+ },
+ {
+ "name": "Benjamin Eberlei",
+ "email": "kontakt@beberlei.de"
+ },
+ {
+ "name": "Jonathan Wage",
+ "email": "jonwage@gmail.com"
+ },
+ {
+ "name": "Johannes Schmitt",
+ "email": "schmittjoh@gmail.com"
+ }
+ ],
+ "description": "Docblock Annotations Parser",
+ "homepage": "https://www.doctrine-project.org/projects/annotations.html",
+ "keywords": [
+ "annotations",
+ "docblock",
+ "parser"
+ ],
+ "support": {
+ "issues": "https://github.com/doctrine/annotations/issues",
+ "source": "https://github.com/doctrine/annotations/tree/1.13.3"
+ },
+ "time": "2022-07-02T10:48:51+00:00"
+ },
+ {
+ "name": "doctrine/lexer",
+ "version": "1.2.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/doctrine/lexer.git",
+ "reference": "c268e882d4dbdd85e36e4ad69e02dc284f89d229"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/doctrine/lexer/zipball/c268e882d4dbdd85e36e4ad69e02dc284f89d229",
+ "reference": "c268e882d4dbdd85e36e4ad69e02dc284f89d229",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^7.1 || ^8.0"
+ },
+ "require-dev": {
+ "doctrine/coding-standard": "^9.0",
+ "phpstan/phpstan": "^1.3",
+ "phpunit/phpunit": "^7.5 || ^8.5 || ^9.5",
+ "vimeo/psalm": "^4.11"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Doctrine\\Common\\Lexer\\": "lib/Doctrine/Common/Lexer"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Guilherme Blanco",
+ "email": "guilhermeblanco@gmail.com"
+ },
+ {
+ "name": "Roman Borschel",
+ "email": "roman@code-factory.org"
+ },
+ {
+ "name": "Johannes Schmitt",
+ "email": "schmittjoh@gmail.com"
+ }
+ ],
+ "description": "PHP Doctrine Lexer parser library that can be used in Top-Down, Recursive Descent Parsers.",
+ "homepage": "https://www.doctrine-project.org/projects/lexer.html",
+ "keywords": [
+ "annotations",
+ "docblock",
+ "lexer",
+ "parser",
+ "php"
+ ],
+ "support": {
+ "issues": "https://github.com/doctrine/lexer/issues",
+ "source": "https://github.com/doctrine/lexer/tree/1.2.3"
+ },
+ "funding": [
+ {
+ "url": "https://www.doctrine-project.org/sponsorship.html",
+ "type": "custom"
+ },
+ {
+ "url": "https://www.patreon.com/phpdoctrine",
+ "type": "patreon"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/doctrine%2Flexer",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-02-28T11:07:21+00:00"
+ },
+ {
+ "name": "friendsofphp/php-cs-fixer",
+ "version": "v3.9.5",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/FriendsOfPHP/PHP-CS-Fixer.git",
+ "reference": "4465d70ba776806857a1ac2a6f877e582445ff36"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/FriendsOfPHP/PHP-CS-Fixer/zipball/4465d70ba776806857a1ac2a6f877e582445ff36",
+ "reference": "4465d70ba776806857a1ac2a6f877e582445ff36",
+ "shasum": ""
+ },
+ "require": {
+ "composer/semver": "^3.2",
+ "composer/xdebug-handler": "^3.0.3",
+ "doctrine/annotations": "^1.13",
+ "ext-json": "*",
+ "ext-tokenizer": "*",
+ "php": "^7.4 || ^8.0",
+ "php-cs-fixer/diff": "^2.0",
+ "symfony/console": "^5.4 || ^6.0",
+ "symfony/event-dispatcher": "^5.4 || ^6.0",
+ "symfony/filesystem": "^5.4 || ^6.0",
+ "symfony/finder": "^5.4 || ^6.0",
+ "symfony/options-resolver": "^5.4 || ^6.0",
+ "symfony/polyfill-mbstring": "^1.23",
+ "symfony/polyfill-php80": "^1.25",
+ "symfony/polyfill-php81": "^1.25",
+ "symfony/process": "^5.4 || ^6.0",
+ "symfony/stopwatch": "^5.4 || ^6.0"
+ },
+ "require-dev": {
+ "justinrainbow/json-schema": "^5.2",
+ "keradus/cli-executor": "^1.5",
+ "mikey179/vfsstream": "^1.6.10",
+ "php-coveralls/php-coveralls": "^2.5.2",
+ "php-cs-fixer/accessible-object": "^1.1",
+ "php-cs-fixer/phpunit-constraint-isidenticalstring": "^1.2",
+ "php-cs-fixer/phpunit-constraint-xmlmatchesxsd": "^1.2.1",
+ "phpspec/prophecy": "^1.15",
+ "phpspec/prophecy-phpunit": "^2.0",
+ "phpunit/phpunit": "^9.5",
+ "phpunitgoodpractices/polyfill": "^1.5",
+ "phpunitgoodpractices/traits": "^1.9.1",
+ "symfony/phpunit-bridge": "^6.0",
+ "symfony/yaml": "^5.4 || ^6.0"
+ },
+ "suggest": {
+ "ext-dom": "For handling output formats in XML",
+ "ext-mbstring": "For handling non-UTF8 characters."
+ },
+ "bin": [
+ "php-cs-fixer"
+ ],
+ "type": "application",
+ "autoload": {
+ "psr-4": {
+ "PhpCsFixer\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Dariusz Rumiński",
+ "email": "dariusz.ruminski@gmail.com"
+ }
+ ],
+ "description": "A tool to automatically fix PHP code style",
+ "support": {
+ "issues": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/issues",
+ "source": "https://github.com/FriendsOfPHP/PHP-CS-Fixer/tree/v3.9.5"
+ },
+ "funding": [
+ {
+ "url": "https://github.com/keradus",
+ "type": "github"
+ }
+ ],
+ "time": "2022-07-22T08:43:51+00:00"
+ },
+ {
+ "name": "php-cs-fixer/diff",
+ "version": "v2.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/PHP-CS-Fixer/diff.git",
+ "reference": "29dc0d507e838c4580d018bd8b5cb412474f7ec3"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/PHP-CS-Fixer/diff/zipball/29dc0d507e838c4580d018bd8b5cb412474f7ec3",
+ "reference": "29dc0d507e838c4580d018bd8b5cb412474f7ec3",
+ "shasum": ""
+ },
+ "require": {
+ "php": "^5.6 || ^7.0 || ^8.0"
+ },
+ "require-dev": {
+ "phpunit/phpunit": "^5.7.23 || ^6.4.3 || ^7.0",
+ "symfony/process": "^3.3"
+ },
+ "type": "library",
+ "autoload": {
+ "classmap": [
+ "src/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "BSD-3-Clause"
+ ],
+ "authors": [
+ {
+ "name": "Sebastian Bergmann",
+ "email": "sebastian@phpunit.de"
+ },
+ {
+ "name": "Kore Nordmann",
+ "email": "mail@kore-nordmann.de"
+ }
+ ],
+ "description": "sebastian/diff v3 backport support for PHP 5.6+",
+ "homepage": "https://github.com/PHP-CS-Fixer",
+ "keywords": [
+ "diff"
+ ],
+ "support": {
+ "issues": "https://github.com/PHP-CS-Fixer/diff/issues",
+ "source": "https://github.com/PHP-CS-Fixer/diff/tree/v2.0.2"
+ },
+ "time": "2020-10-14T08:32:19+00:00"
+ },
+ {
+ "name": "psr/cache",
+ "version": "3.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/cache.git",
+ "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/cache/zipball/aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
+ "reference": "aa5030cfa5405eccfdcb1083ce040c2cb8d253bf",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.0.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Cache\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for caching libraries",
+ "keywords": [
+ "cache",
+ "psr",
+ "psr-6"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/cache/tree/3.0.0"
+ },
+ "time": "2021-02-03T23:26:27+00:00"
+ },
+ {
+ "name": "psr/container",
+ "version": "2.0.2",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/container.git",
+ "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/container/zipball/c71ecc56dfe541dbd90c5360474fbc405f8d5963",
+ "reference": "c71ecc56dfe541dbd90c5360474fbc405f8d5963",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.4.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "2.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Container\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common Container Interface (PHP FIG PSR-11)",
+ "homepage": "https://github.com/php-fig/container",
+ "keywords": [
+ "PSR-11",
+ "container",
+ "container-interface",
+ "container-interop",
+ "psr"
+ ],
+ "support": {
+ "issues": "https://github.com/php-fig/container/issues",
+ "source": "https://github.com/php-fig/container/tree/2.0.2"
+ },
+ "time": "2021-11-05T16:47:00+00:00"
+ },
+ {
+ "name": "psr/event-dispatcher",
+ "version": "1.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/event-dispatcher.git",
+ "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/event-dispatcher/zipball/dbefd12671e8a14ec7f180cab83036ed26714bb0",
+ "reference": "dbefd12671e8a14ec7f180cab83036ed26714bb0",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.2.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "1.0.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\EventDispatcher\\": "src/"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "http://www.php-fig.org/"
+ }
+ ],
+ "description": "Standard interfaces for event handling.",
+ "keywords": [
+ "events",
+ "psr",
+ "psr-14"
+ ],
+ "support": {
+ "issues": "https://github.com/php-fig/event-dispatcher/issues",
+ "source": "https://github.com/php-fig/event-dispatcher/tree/1.0.0"
+ },
+ "time": "2019-01-08T18:20:26+00:00"
+ },
+ {
+ "name": "psr/log",
+ "version": "3.0.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/php-fig/log.git",
+ "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/php-fig/log/zipball/fe5ea303b0887d5caefd3d431c3e61ad47037001",
+ "reference": "fe5ea303b0887d5caefd3d431c3e61ad47037001",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.0.0"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-master": "3.x-dev"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Psr\\Log\\": "src"
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "PHP-FIG",
+ "homepage": "https://www.php-fig.org/"
+ }
+ ],
+ "description": "Common interface for logging libraries",
+ "homepage": "https://github.com/php-fig/log",
+ "keywords": [
+ "log",
+ "psr",
+ "psr-3"
+ ],
+ "support": {
+ "source": "https://github.com/php-fig/log/tree/3.0.0"
+ },
+ "time": "2021-07-14T16:46:02+00:00"
+ },
+ {
+ "name": "symfony/console",
+ "version": "v6.1.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/console.git",
+ "reference": "43fcb5c5966b43c56bcfa481368d90d748936ab8"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/console/zipball/43fcb5c5966b43c56bcfa481368d90d748936ab8",
+ "reference": "43fcb5c5966b43c56bcfa481368d90d748936ab8",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/deprecation-contracts": "^2.1|^3",
+ "symfony/polyfill-mbstring": "~1.0",
+ "symfony/service-contracts": "^1.1|^2|^3",
+ "symfony/string": "^5.4|^6.0"
+ },
+ "conflict": {
+ "symfony/dependency-injection": "<5.4",
+ "symfony/dotenv": "<5.4",
+ "symfony/event-dispatcher": "<5.4",
+ "symfony/lock": "<5.4",
+ "symfony/process": "<5.4"
+ },
+ "provide": {
+ "psr/log-implementation": "1.0|2.0|3.0"
+ },
+ "require-dev": {
+ "psr/log": "^1|^2|^3",
+ "symfony/config": "^5.4|^6.0",
+ "symfony/dependency-injection": "^5.4|^6.0",
+ "symfony/event-dispatcher": "^5.4|^6.0",
+ "symfony/lock": "^5.4|^6.0",
+ "symfony/process": "^5.4|^6.0",
+ "symfony/var-dumper": "^5.4|^6.0"
+ },
+ "suggest": {
+ "psr/log": "For using the console logger",
+ "symfony/event-dispatcher": "",
+ "symfony/lock": "",
+ "symfony/process": ""
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Console\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Eases the creation of beautiful and testable command line interfaces",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "cli",
+ "command line",
+ "console",
+ "terminal"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/console/tree/v6.1.3"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-07-22T14:17:57+00:00"
+ },
+ {
+ "name": "symfony/deprecation-contracts",
+ "version": "v3.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/deprecation-contracts.git",
+ "reference": "07f1b9cc2ffee6aaafcf4b710fbc38ff736bd918"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/deprecation-contracts/zipball/07f1b9cc2ffee6aaafcf4b710fbc38ff736bd918",
+ "reference": "07f1b9cc2ffee6aaafcf4b710fbc38ff736bd918",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.1-dev"
+ },
+ "thanks": {
+ "name": "symfony/contracts",
+ "url": "https://github.com/symfony/contracts"
+ }
+ },
+ "autoload": {
+ "files": [
+ "function.php"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "A generic function and convention to trigger deprecation notices",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/deprecation-contracts/tree/v3.1.1"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-02-25T11:15:52+00:00"
+ },
+ {
+ "name": "symfony/event-dispatcher",
+ "version": "v6.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/event-dispatcher.git",
+ "reference": "a0449a7ad7daa0f7c0acd508259f80544ab5a347"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/event-dispatcher/zipball/a0449a7ad7daa0f7c0acd508259f80544ab5a347",
+ "reference": "a0449a7ad7daa0f7c0acd508259f80544ab5a347",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/event-dispatcher-contracts": "^2|^3"
+ },
+ "conflict": {
+ "symfony/dependency-injection": "<5.4"
+ },
+ "provide": {
+ "psr/event-dispatcher-implementation": "1.0",
+ "symfony/event-dispatcher-implementation": "2.0|3.0"
+ },
+ "require-dev": {
+ "psr/log": "^1|^2|^3",
+ "symfony/config": "^5.4|^6.0",
+ "symfony/dependency-injection": "^5.4|^6.0",
+ "symfony/error-handler": "^5.4|^6.0",
+ "symfony/expression-language": "^5.4|^6.0",
+ "symfony/http-foundation": "^5.4|^6.0",
+ "symfony/service-contracts": "^1.1|^2|^3",
+ "symfony/stopwatch": "^5.4|^6.0"
+ },
+ "suggest": {
+ "symfony/dependency-injection": "",
+ "symfony/http-kernel": ""
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\EventDispatcher\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides tools that allow your application components to communicate with each other by dispatching events and listening to them",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/event-dispatcher/tree/v6.1.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-05-05T16:51:07+00:00"
+ },
+ {
+ "name": "symfony/event-dispatcher-contracts",
+ "version": "v3.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/event-dispatcher-contracts.git",
+ "reference": "02ff5eea2f453731cfbc6bc215e456b781480448"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/event-dispatcher-contracts/zipball/02ff5eea2f453731cfbc6bc215e456b781480448",
+ "reference": "02ff5eea2f453731cfbc6bc215e456b781480448",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "psr/event-dispatcher": "^1"
+ },
+ "suggest": {
+ "symfony/event-dispatcher-implementation": ""
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.1-dev"
+ },
+ "thanks": {
+ "name": "symfony/contracts",
+ "url": "https://github.com/symfony/contracts"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Contracts\\EventDispatcher\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Generic abstractions related to dispatching event",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "abstractions",
+ "contracts",
+ "decoupling",
+ "interfaces",
+ "interoperability",
+ "standards"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/event-dispatcher-contracts/tree/v3.1.1"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-02-25T11:15:52+00:00"
+ },
+ {
+ "name": "symfony/filesystem",
+ "version": "v6.1.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/filesystem.git",
+ "reference": "c780e677cddda78417fa5187a7c6cd2f21110db9"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/filesystem/zipball/c780e677cddda78417fa5187a7c6cd2f21110db9",
+ "reference": "c780e677cddda78417fa5187a7c6cd2f21110db9",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/polyfill-ctype": "~1.8",
+ "symfony/polyfill-mbstring": "~1.8"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Filesystem\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides basic utilities for the filesystem",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/filesystem/tree/v6.1.3"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-07-20T14:45:06+00:00"
+ },
+ {
+ "name": "symfony/finder",
+ "version": "v6.1.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/finder.git",
+ "reference": "39696bff2c2970b3779a5cac7bf9f0b88fc2b709"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/finder/zipball/39696bff2c2970b3779a5cac7bf9f0b88fc2b709",
+ "reference": "39696bff2c2970b3779a5cac7bf9f0b88fc2b709",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "require-dev": {
+ "symfony/filesystem": "^6.0"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Finder\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Finds files and directories via an intuitive fluent interface",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/finder/tree/v6.1.3"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-07-29T07:42:06+00:00"
+ },
+ {
+ "name": "symfony/options-resolver",
+ "version": "v6.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/options-resolver.git",
+ "reference": "a3016f5442e28386ded73c43a32a5b68586dd1c4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/options-resolver/zipball/a3016f5442e28386ded73c43a32a5b68586dd1c4",
+ "reference": "a3016f5442e28386ded73c43a32a5b68586dd1c4",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/deprecation-contracts": "^2.1|^3"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\OptionsResolver\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides an improved replacement for the array_replace PHP function",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "config",
+ "configuration",
+ "options"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/options-resolver/tree/v6.1.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-02-25T11:15:52+00:00"
+ },
+ {
+ "name": "symfony/polyfill-ctype",
+ "version": "v1.26.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-ctype.git",
+ "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4",
+ "reference": "6fd1b9a79f6e3cf65f9e679b23af304cd9e010d4",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "provide": {
+ "ext-ctype": "*"
+ },
+ "suggest": {
+ "ext-ctype": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.26-dev"
+ },
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Ctype\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Gert de Pagter",
+ "email": "BackEndTea@gmail.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for ctype functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "ctype",
+ "polyfill",
+ "portable"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-ctype/tree/v1.26.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-05-24T11:49:31+00:00"
+ },
+ {
+ "name": "symfony/polyfill-intl-grapheme",
+ "version": "v1.26.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-intl-grapheme.git",
+ "reference": "433d05519ce6990bf3530fba6957499d327395c2"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-grapheme/zipball/433d05519ce6990bf3530fba6957499d327395c2",
+ "reference": "433d05519ce6990bf3530fba6957499d327395c2",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "suggest": {
+ "ext-intl": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.26-dev"
+ },
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Intl\\Grapheme\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for intl's grapheme_* functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "grapheme",
+ "intl",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-intl-grapheme/tree/v1.26.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-05-24T11:49:31+00:00"
+ },
+ {
+ "name": "symfony/polyfill-intl-normalizer",
+ "version": "v1.26.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-intl-normalizer.git",
+ "reference": "219aa369ceff116e673852dce47c3a41794c14bd"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-intl-normalizer/zipball/219aa369ceff116e673852dce47c3a41794c14bd",
+ "reference": "219aa369ceff116e673852dce47c3a41794c14bd",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "suggest": {
+ "ext-intl": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.26-dev"
+ },
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Intl\\Normalizer\\": ""
+ },
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for intl's Normalizer class and related functions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "intl",
+ "normalizer",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-intl-normalizer/tree/v1.26.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-05-24T11:49:31+00:00"
+ },
+ {
+ "name": "symfony/polyfill-mbstring",
+ "version": "v1.26.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-mbstring.git",
+ "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e",
+ "reference": "9344f9cb97f3b19424af1a21a3b0e75b0a7d8d7e",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "provide": {
+ "ext-mbstring": "*"
+ },
+ "suggest": {
+ "ext-mbstring": "For best performance"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.26-dev"
+ },
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Mbstring\\": ""
+ }
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill for the Mbstring extension",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "mbstring",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-mbstring/tree/v1.26.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-05-24T11:49:31+00:00"
+ },
+ {
+ "name": "symfony/polyfill-php80",
+ "version": "v1.26.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php80.git",
+ "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php80/zipball/cfa0ae98841b9e461207c13ab093d76b0fa7bace",
+ "reference": "cfa0ae98841b9e461207c13ab093d76b0fa7bace",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.26-dev"
+ },
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Php80\\": ""
+ },
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Ion Bazan",
+ "email": "ion.bazan@gmail.com"
+ },
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill backporting some PHP 8.0+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-php80/tree/v1.26.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-05-10T07:21:04+00:00"
+ },
+ {
+ "name": "symfony/polyfill-php81",
+ "version": "v1.26.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/polyfill-php81.git",
+ "reference": "13f6d1271c663dc5ae9fb843a8f16521db7687a1"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/polyfill-php81/zipball/13f6d1271c663dc5ae9fb843a8f16521db7687a1",
+ "reference": "13f6d1271c663dc5ae9fb843a8f16521db7687a1",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=7.1"
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "1.26-dev"
+ },
+ "thanks": {
+ "name": "symfony/polyfill",
+ "url": "https://github.com/symfony/polyfill"
+ }
+ },
+ "autoload": {
+ "files": [
+ "bootstrap.php"
+ ],
+ "psr-4": {
+ "Symfony\\Polyfill\\Php81\\": ""
+ },
+ "classmap": [
+ "Resources/stubs"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Symfony polyfill backporting some PHP 8.1+ features to lower PHP versions",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "compatibility",
+ "polyfill",
+ "portable",
+ "shim"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/polyfill-php81/tree/v1.26.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-05-24T11:49:31+00:00"
+ },
+ {
+ "name": "symfony/process",
+ "version": "v6.1.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/process.git",
+ "reference": "a6506e99cfad7059b1ab5cab395854a0a0c21292"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/process/zipball/a6506e99cfad7059b1ab5cab395854a0a0c21292",
+ "reference": "a6506e99cfad7059b1ab5cab395854a0a0c21292",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Process\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Executes commands in sub-processes",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/process/tree/v6.1.3"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-06-27T17:24:16+00:00"
+ },
+ {
+ "name": "symfony/service-contracts",
+ "version": "v3.1.1",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/service-contracts.git",
+ "reference": "925e713fe8fcacf6bc05e936edd8dd5441a21239"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/service-contracts/zipball/925e713fe8fcacf6bc05e936edd8dd5441a21239",
+ "reference": "925e713fe8fcacf6bc05e936edd8dd5441a21239",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "psr/container": "^2.0"
+ },
+ "conflict": {
+ "ext-psr": "<1.1|>=2"
+ },
+ "suggest": {
+ "symfony/service-implementation": ""
+ },
+ "type": "library",
+ "extra": {
+ "branch-alias": {
+ "dev-main": "3.1-dev"
+ },
+ "thanks": {
+ "name": "symfony/contracts",
+ "url": "https://github.com/symfony/contracts"
+ }
+ },
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Contracts\\Service\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Test/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Generic abstractions related to writing services",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "abstractions",
+ "contracts",
+ "decoupling",
+ "interfaces",
+ "interoperability",
+ "standards"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/service-contracts/tree/v3.1.1"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-05-30T19:18:58+00:00"
+ },
+ {
+ "name": "symfony/stopwatch",
+ "version": "v6.1.0",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/stopwatch.git",
+ "reference": "77dedae82ce2a26e2e9b481855473fc3b3e4e54d"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/stopwatch/zipball/77dedae82ce2a26e2e9b481855473fc3b3e4e54d",
+ "reference": "77dedae82ce2a26e2e9b481855473fc3b3e4e54d",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/service-contracts": "^1|^2|^3"
+ },
+ "type": "library",
+ "autoload": {
+ "psr-4": {
+ "Symfony\\Component\\Stopwatch\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Fabien Potencier",
+ "email": "fabien@symfony.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides a way to profile code",
+ "homepage": "https://symfony.com",
+ "support": {
+ "source": "https://github.com/symfony/stopwatch/tree/v6.1.0"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-02-25T11:15:52+00:00"
+ },
+ {
+ "name": "symfony/string",
+ "version": "v6.1.3",
+ "source": {
+ "type": "git",
+ "url": "https://github.com/symfony/string.git",
+ "reference": "f35241f45c30bcd9046af2bb200a7086f70e1d6b"
+ },
+ "dist": {
+ "type": "zip",
+ "url": "https://api.github.com/repos/symfony/string/zipball/f35241f45c30bcd9046af2bb200a7086f70e1d6b",
+ "reference": "f35241f45c30bcd9046af2bb200a7086f70e1d6b",
+ "shasum": ""
+ },
+ "require": {
+ "php": ">=8.1",
+ "symfony/polyfill-ctype": "~1.8",
+ "symfony/polyfill-intl-grapheme": "~1.0",
+ "symfony/polyfill-intl-normalizer": "~1.0",
+ "symfony/polyfill-mbstring": "~1.0"
+ },
+ "conflict": {
+ "symfony/translation-contracts": "<2.0"
+ },
+ "require-dev": {
+ "symfony/error-handler": "^5.4|^6.0",
+ "symfony/http-client": "^5.4|^6.0",
+ "symfony/translation-contracts": "^2.0|^3.0",
+ "symfony/var-exporter": "^5.4|^6.0"
+ },
+ "type": "library",
+ "autoload": {
+ "files": [
+ "Resources/functions.php"
+ ],
+ "psr-4": {
+ "Symfony\\Component\\String\\": ""
+ },
+ "exclude-from-classmap": [
+ "/Tests/"
+ ]
+ },
+ "notification-url": "https://packagist.org/downloads/",
+ "license": [
+ "MIT"
+ ],
+ "authors": [
+ {
+ "name": "Nicolas Grekas",
+ "email": "p@tchwork.com"
+ },
+ {
+ "name": "Symfony Community",
+ "homepage": "https://symfony.com/contributors"
+ }
+ ],
+ "description": "Provides an object-oriented API to strings and deals with bytes, UTF-8 code points and grapheme clusters in a unified way",
+ "homepage": "https://symfony.com",
+ "keywords": [
+ "grapheme",
+ "i18n",
+ "string",
+ "unicode",
+ "utf-8",
+ "utf8"
+ ],
+ "support": {
+ "source": "https://github.com/symfony/string/tree/v6.1.3"
+ },
+ "funding": [
+ {
+ "url": "https://symfony.com/sponsor",
+ "type": "custom"
+ },
+ {
+ "url": "https://github.com/fabpot",
+ "type": "github"
+ },
+ {
+ "url": "https://tidelift.com/funding/github/packagist/symfony/symfony",
+ "type": "tidelift"
+ }
+ ],
+ "time": "2022-07-27T15:50:51+00:00"
+ }
+ ],
+ "packages-dev": [],
+ "aliases": [],
+ "minimum-stability": "stable",
+ "stability-flags": [],
+ "prefer-stable": false,
+ "prefer-lowest": false,
+ "platform": [],
+ "platform-dev": [],
+ "plugin-api-version": "2.3.0"
+}