diff --git a/core/Authenticator/LoginFormAuthenticator.php b/core/Authenticator/LoginFormAuthenticator.php new file mode 100644 index 0000000..193c61f --- /dev/null +++ b/core/Authenticator/LoginFormAuthenticator.php @@ -0,0 +1,96 @@ +entityManager = $entityManager; + $this->urlGenerator = $urlGenerator; + $this->csrfTokenManager = $csrfTokenManager; + $this->passwordEncoder = $passwordEncoder; + } + + public function supports(Request $request) + { + return 'auth_login' === $request->attributes->get('_route') && $request->isMethod('POST'); + } + + public function getCredentials(Request $request) + { + $credentials = [ + 'email' => $request->request->get('_username'), + 'password' => $request->request->get('_password'), + 'csrf_token' => $request->request->get('_csrf_token'), + ]; + + $request->getSession()->set(Security::LAST_USERNAME, $credentials['email']); + + return $credentials; + } + + public function getUser($credentials, UserProviderInterface $userProvider) + { + $token = new CsrfToken('authenticate', $credentials['csrf_token']); + + if (!$this->csrfTokenManager->isTokenValid($token)) { + throw new InvalidCsrfTokenException(); + } + + $user = $this->entityManager->getRepository(User::class)->findOneBy(['email' => $credentials['email']]); + + if (!$user) { + // fail authentication with a custom error + throw new CustomUserMessageAuthenticationException('Email could not be found.'); + } + + return $user; + } + + public function checkCredentials($credentials, UserInterface $user) + { + return $this->passwordEncoder->isPasswordValid($user, $credentials['password']); + } + + public function onAuthenticationSuccess(Request $request, TokenInterface $token, $providerKey) + { + if ($targetPath = $this->getTargetPath($request->getSession(), $providerKey)) { + return new RedirectResponse($targetPath); + } + + return new RedirectResponse($this->urlGenerator->generate('admin_dashboard_index')); + } + + protected function getLoginUrl() + { + return $this->urlGenerator->generate('auth_login'); + } +} diff --git a/core/Bundle/AppBundle.php b/core/Bundle/AppBundle.php new file mode 100644 index 0000000..a0da128 --- /dev/null +++ b/core/Bundle/AppBundle.php @@ -0,0 +1,23 @@ +getUser(); + + return $this->render('account/admin/edit.html.twig', [ + 'account' => $account, + ]); + } + + /** + * @Route("/2fa", name="admin_account_2fa") + */ + public function twoFactorAuthentication( + Request $request, + GoogleAuthenticatorInterface $totpAuthenticatorService, + EntityManager $entityManager + ): Response { + if ($request->isMethod('GET')) { + return $this->redirectToRoute('admin_account'); + } + + $account = $this->getUser(); + $csrfToken = $request->request->get('_csrf_token'); + $enable = (bool) $request->request->get('enable'); + $code = $request->request->get('code', ''); + $secret = $request->request->get('secret', ''); + $qrCodeContent = null; + + if ($this->isCsrfTokenValid('2fa', $csrfToken)) { + if ($enable && !$account->isTotpAuthenticationEnabled()) { + if (empty($secret)) { + $secret = $totpAuthenticatorService->generateSecret(); + + $account->setTotpSecret($secret); + + $qrCodeContent = $totpAuthenticatorService->getQRContent($account); + } else { + $account->setTotpSecret($secret); + + $qrCodeContent = $totpAuthenticatorService->getQRContent($account); + + if (!$totpAuthenticatorService->checkCode($account, $code)) { + $this->addFlash('error', 'Le code n\'est pas valide.'); + } else { + $this->addFlash('success', 'Double authentification activée.'); + + $entityManager->update($account); + + return $this->redirectToRoute('admin_account'); + } + } + } + + if (!$enable && $account->isTotpAuthenticationEnabled()) { + $account->setTotpSecret(null); + + $entityManager->update($account); + + $this->addFlash('success', 'Double authentification désactivée.'); + + return $this->redirectToRoute('admin_account'); + } + } + + return $this->render('account/admin/edit.html.twig', [ + 'account' => $account, + 'twoFaKey' => $secret, + 'twoFaQrCodeContent' => $qrCodeContent, + ]); + } + + /** + * @Route("/password", name="admin_account_password", methods={"POST"}) + */ + public function password( + Request $request, + UserRepository $repository, + TokenGeneratorInterface $tokenGenerator, + UserPasswordEncoderInterface $encoder, + EntityManager $entityManager + ): Response { + $account = $this->getUser(); + $csrfToken = $request->request->get('_csrf_token'); + + if ($this->isCsrfTokenValid('password', $csrfToken)) { + $password = $request->request->get('password'); + + if (!$encoder->isPasswordValid($account, $password)) { + $this->addFlash('error', 'Le formulaire n\'est pas valide.'); + + return $this->redirectToRoute('admin_account'); + } + + $password1 = $request->request->get('password1'); + $password2 = $request->request->get('password2'); + + $zxcvbn = new Zxcvbn(); + $strength = $zxcvbn->passwordStrength($password1, []); + + if (4 === $strength['score'] && $password1 === $password2) { + $account + ->setPassword($encoder->encodePassword( + $account, + $password1 + )) + ->setConfirmationToken($tokenGenerator->generateToken()) + ; + + $entityManager->update($account); + + $this->addFlash('success', 'Mot de passe modifié !'); + + return $this->redirectToRoute('admin_account'); + } + } + + $this->addFlash('error', 'Le formulaire n\'est pas valide.'); + + return $this->redirectToRoute('admin_account'); + } + + /** + * {@inheritdoc} + */ + protected function getSection(): string + { + return 'account'; + } +} diff --git a/core/Controller/Admin/AdminController.php b/core/Controller/Admin/AdminController.php new file mode 100644 index 0000000..e007d34 --- /dev/null +++ b/core/Controller/Admin/AdminController.php @@ -0,0 +1,21 @@ +getSection(); + + return parent::render($view, $parameters, $response); + } + + abstract protected function getSection(): string; +} diff --git a/core/Controller/Auth/AuthController.php b/core/Controller/Auth/AuthController.php new file mode 100644 index 0000000..153ae17 --- /dev/null +++ b/core/Controller/Auth/AuthController.php @@ -0,0 +1,159 @@ +getUser()) { + return $this->redirectToRoute('admin_dashboard_index'); + } + + $error = $authenticationUtils->getLastAuthenticationError(); + $lastUsername = $authenticationUtils->getLastUsername(); + + return $this->render('auth/login.html.twig', [ + 'last_username' => $lastUsername, + 'error' => $error, + ]); + } + + /** + * @Route("/resetting/request", name="auth_resetting_request") + */ + public function requestResetting( + Request $request, + UserRepository $repository, + TokenGeneratorInterface $tokenGenerator, + EntityManager $entityManager, + EventDispatcherInterface $eventDispatcher + ): Response { + if ($this->getUser()) { + return $this->redirectToRoute('admin_dashboard_index'); + } + + $emailSent = false; + + if ($request->isMethod('POST')) { + $csrfToken = $request->request->get('_csrf_token'); + + if ($this->isCsrfTokenValid('resetting_request', $csrfToken)) { + $username = trim((string) $request->request->get('username')); + + if ($username) { + $account = $repository->findOneByEmail($username); + + if ($account) { + $passwordRequestedAt = $account->getPasswordRequestedAt(); + + if (null !== $passwordRequestedAt && $passwordRequestedAt->getTimestamp() > (time() - 3600 / 2)) { + $emailSent = true; + } + + if (!$emailSent) { + $account->setConfirmationToken($tokenGenerator->generateToken()); + $account->setPasswordRequestedAt(new \DateTime('now')); + + $entityManager->update($account); + $eventDispatcher->dispatch(new PasswordRequestEvent($account), PasswordRequestEvent::EVENT); + + $emailSent = true; + } + } + } + } + } + + return $this->render('auth/resetting_request.html.twig', [ + 'email_sent' => $emailSent, + ]); + } + + /** + * @Route("/resetting/update/{token}", name="auth_resetting_update") + */ + public function requestUpdate( + string $token, + Request $request, + UserRepository $repository, + TokenGeneratorInterface $tokenGenerator, + UserPasswordEncoderInterface $encoder, + EntityManager $entityManager + ): Response { + if ($this->getUser()) { + return $this->redirectToRoute('admin_dashboard_index'); + } + + $account = $repository->findOneByConfirmationToken($token); + $passwordUpdated = false; + $expired = false; + + if ($account) { + $passwordRequestedAt = $account->getPasswordRequestedAt(); + + if (null !== $passwordRequestedAt && $passwordRequestedAt->getTimestamp() < (time() - 3600 * 2)) { + $expired = true; + } + } else { + $expired = true; + } + + if ($request->isMethod('POST') && !$expired) { + $csrfToken = $request->request->get('_csrf_token'); + + if ($this->isCsrfTokenValid('resetting_update', $csrfToken)) { + $password = $request->request->get('password'); + $password2 = $request->request->get('password2'); + + $zxcvbn = new Zxcvbn(); + $strength = $zxcvbn->passwordStrength($password, []); + + if (4 === $strength['score'] && $password === $password2) { + $account + ->setPassword($encoder->encodePassword( + $account, + $password + )) + ->setConfirmationToken($tokenGenerator->generateToken()) + ->setPasswordRequestedAt(new \DateTime('now')) + ; + + $entityManager->update($account); + + $passwordUpdated = true; + } + } + } + + return $this->render('auth/resetting_update.html.twig', [ + 'password_updated' => $passwordUpdated, + 'token' => $token, + 'expired' => $expired, + ]); + } + + /** + * @Route("/logout", name="auth_logout") + */ + public function logout() + { + throw new \Exception('This method can be blank - it will be intercepted by the logout key on your firewall'); + } +} diff --git a/core/Controller/Dashboard/DashboardAdminController.php b/core/Controller/Dashboard/DashboardAdminController.php new file mode 100644 index 0000000..a514bd5 --- /dev/null +++ b/core/Controller/Dashboard/DashboardAdminController.php @@ -0,0 +1,27 @@ +render('dashboard/admin/index.html.twig', [ + ]); + } + + protected function getSection(): string + { + return 'dashboard'; + } +} diff --git a/core/Controller/Site/MenuAdminController.php b/core/Controller/Site/MenuAdminController.php new file mode 100644 index 0000000..20d65d8 --- /dev/null +++ b/core/Controller/Site/MenuAdminController.php @@ -0,0 +1,82 @@ +create($navigation); + $form = $this->createForm(EntityType::class, $entity); + $form->handleRequest($request); + + if ($form->isValid()) { + $entityManager->create($entity); + + $this->addFlash('success', 'Donnée enregistrée.'); + } else { + $this->addFlash('warning', 'Le formulaire est invalide.'); + } + + return $this->redirectToRoute('admin_site_tree_navigation', [ + 'navigation' => $navigation->getId(), + ]); + } + + /** + * @Route("/edit/{entity}", name="admin_site_menu_edit", methods={"POST"}) + */ + public function edit(Entity $entity, EntityManager $entityManager, Request $request): Response + { + $form = $this->createForm(EntityType::class, $entity); + $form->handleRequest($request); + + if ($form->isValid()) { + $entityManager->update($entity); + $this->addFlash('success', 'Donnée enregistrée.'); + } else { + $this->addFlash('warning', 'Le formulaire est invalide.'); + } + + return $this->redirectToRoute('admin_site_tree_navigation', [ + 'navigation' => $entity->getNavigation()->getId(), + ]); + } + + /** + * @Route("/delete/{entity}", name="admin_site_menu_delete", methods={"DELETE"}) + */ + public function delete(Entity $entity, EntityManager $entityManager, Request $request): Response + { + if ($this->isCsrfTokenValid('delete'.$entity->getId(), $request->request->get('_token'))) { + $entityManager->delete($entity); + + $this->addFlash('success', 'Données supprimée..'); + } + + return $this->redirectToRoute('admin_site_tree_navigation', [ + 'navigation' => $entity->getNavigation()->getId(), + ]); + } + + public function getSection(): string + { + return ''; + } +} diff --git a/core/Controller/Site/NavigationAdminController.php b/core/Controller/Site/NavigationAdminController.php new file mode 100644 index 0000000..1e3af7e --- /dev/null +++ b/core/Controller/Site/NavigationAdminController.php @@ -0,0 +1,116 @@ +paginate($page); + + return $this->render('site/navigation_admin/index.html.twig', [ + 'pager' => $pager, + ]); + } + + /** + * @Route("/new", name="admin_site_navigation_new") + */ + public function new(EntityFactory $factory, EntityManager $entityManager, Request $request): Response + { + $entity = $factory->create(); + $form = $this->createForm(EntityType::class, $entity); + + if ($request->isMethod('POST')) { + $form->handleRequest($request); + + if ($form->isValid()) { + $entityManager->create($entity); + $this->addFlash('success', 'Donnée enregistrée.'); + + return $this->redirectToRoute('admin_site_navigation_edit', [ + 'entity' => $entity->getId(), + ]); + } + $this->addFlash('warning', 'Le formulaire est invalide.'); + } + + return $this->render('site/navigation_admin/new.html.twig', [ + 'form' => $form->createView(), + 'entity' => $entity, + ]); + } + + /** + * @Route("/edit/{entity}", name="admin_site_navigation_edit") + */ + public function edit(Entity $entity, EntityManager $entityManager, Request $request): Response + { + $form = $this->createForm(EntityType::class, $entity); + + if ($request->isMethod('POST')) { + $form->handleRequest($request); + + if ($form->isValid()) { + $entityManager->update($entity); + $this->addFlash('success', 'Donnée enregistrée.'); + + return $this->redirectToRoute('admin_site_navigation_edit', [ + 'entity' => $entity->getId(), + ]); + } + + $this->addFlash('warning', 'Le formulaire est invalide.'); + } + + return $this->render('site/navigation_admin/edit.html.twig', [ + 'form' => $form->createView(), + 'entity' => $entity, + ]); + } + + /** + * @Route("/show/{entity}", name="admin_site_navigation_show") + */ + public function show(Entity $entity): Response + { + return $this->render('site/navigation_admin/show.html.twig', [ + 'entity' => $entity, + ]); + } + + /** + * @Route("/delete/{entity}", name="admin_site_navigation_delete", methods={"DELETE"}) + */ + public function delete(Entity $entity, EntityManager $entityManager, Request $request): Response + { + if ($this->isCsrfTokenValid('delete'.$entity->getId(), $request->request->get('_token'))) { + $entityManager->delete($entity); + + $this->addFlash('success', 'Données supprimée..'); + } + + return $this->redirectToRoute('admin_site_navigation_index'); + } + + public function getSection(): string + { + return 'site_navigation'; + } +} diff --git a/core/Controller/Site/NodeAdminController.php b/core/Controller/Site/NodeAdminController.php new file mode 100644 index 0000000..ac88933 --- /dev/null +++ b/core/Controller/Site/NodeAdminController.php @@ -0,0 +1,263 @@ +create($node->getMenu()); + $form = $this->createForm(EntityType::class, $entity, [ + 'pages' => $pageLocator->getPages(), + ]); + + if ($request->isMethod('POST')) { + $form->handleRequest($request); + + if ($form->isValid()) { + $position = $form->get('position')->getData(); + + $parent = 'above' === $position ? $node : $node->getParent(); + $entity->setParent($parent); + + if ('above' === $position) { + $nodeRepository->persistAsLastChild($entity, $node); + } else { + if ('after' === $position) { + $nodeRepository->persistAsNextSiblingOf($entity, $node); + } elseif ('before' === $position) { + $nodeRepository->persistAsPrevSiblingOf($entity, $node); + } + } + + $this->handlePageAssociation( + $form->get('pageAction')->getData(), + $form->get('pageEntity')->getData(), + $form->get('pageType')->getData(), + $entity, + $pageFactory, + $pageLocator + ); + + $entityManager->update($entity); + + $this->addFlash('success', 'Donnée enregistrée.'); + } else { + $this->addFlash('warning', 'Le formulaire est invalide.'); + } + + return $this->redirectToRoute('admin_site_tree_navigation', [ + 'navigation' => $node->getMenu()->getNavigation()->getId(), + ]); + } + + return $this->render('site/node_admin/new.html.twig', [ + 'form' => $form->createView(), + 'node' => $node, + 'entity' => $entity, + ]); + } + + /** + * @Route("/edit/{entity}", name="admin_site_node_edit") + */ + public function edit( + Entity $entity, + EntityManager $entityManager, + PageFactory $pageFactory, + PageLocator $pageLocator, + Request $request + ): Response { + $form = $this->createForm(EntityType::class, $entity, [ + 'pages' => $pageLocator->getPages(), + ]); + + if ($request->isMethod('POST')) { + $form->handleRequest($request); + + if ($form->isValid()) { + $this->handlePageAssociation( + $form->get('pageAction')->getData(), + $form->get('pageEntity')->getData(), + $form->get('pageType')->getData(), + $entity, + $pageFactory, + $pageLocator + ); + + $entityManager->update($entity); + + $this->addFlash('success', 'Donnée enregistrée.'); + } else { + $this->addFlash('warning', 'Le formulaire est invalide.'); + } + + return $this->redirectToRoute('admin_site_tree_navigation', [ + 'navigation' => $entity->getMenu()->getNavigation()->getId(), + ]); + } + + return $this->render('site/node_admin/edit.html.twig', [ + 'form' => $form->createView(), + 'entity' => $entity, + ]); + } + + /** + * @Route("/move/{entity}", name="admin_site_node_move") + */ + public function move( + Entity $entity, + EntityManager $entityManager, + NodeRepository $nodeRepository, + Request $request + ): Response { + $form = $this->createForm(NodeMoveType::class, null, [ + 'menu' => $entity->getMenu(), + ]); + + if ($request->isMethod('POST')) { + $form->handleRequest($request); + + if ($form->get('node')->getData()->getId() === $entity->getId()) { + $form->get('node')->addError(new FormError('Élement de référence invalide.')); + } + + if ($form->isValid()) { + $position = $form->get('position')->getData(); + $node = $form->get('node')->getData(); + + $parent = 'above' === $position ? $node : $node->getParent(); + $entity->setParent($parent); + + if ('above' === $position) { + $nodeRepository->persistAsLastChild($entity, $node); + $entityManager->flush(); + } else { + if ('after' === $position) { + $nodeRepository->persistAsNextSiblingOf($entity, $node); + } elseif ('before' === $position) { + $nodeRepository->persistAsPrevSiblingOf($entity, $node); + } + + $entityManager->flush(); + } + + $this->addFlash('success', 'Donnée enregistrée.'); + } else { + $this->addFlash('warning', 'Le formulaire est invalide.'); + } + + return $this->redirectToRoute('admin_site_tree_navigation', [ + 'navigation' => $entity->getMenu()->getNavigation()->getId(), + ]); + } + + return $this->render('site/node_admin/move.html.twig', [ + 'form' => $form->createView(), + 'entity' => $entity, + ]); + } + + /** + * @Route("/toggle/visibility/{entity}", name="admin_site_node_toggle_visibility", methods={"POST"}) + */ + public function toggleVisibility(Entity $entity, EntityManager $entityManager, Request $request): Response + { + if ($this->isCsrfTokenValid('toggle_visibility'.$entity->getId(), $request->request->get('_token'))) { + $entity->setIsVisible(!$entity->getIsVisible()); + + $entityManager->update($entity); + + $this->addFlash('success', 'Donnée enregistrée.'); + } + + return $this->redirectToRoute('admin_site_tree_navigation', [ + 'navigation' => $entity->getMenu()->getNavigation()->getId(), + ]); + } + + /** + * @Route("/delete/{entity}", name="admin_site_node_delete", methods={"DELETE"}) + */ + public function delete( + Entity $entity, + NodeRepository $nodeRepository, + EventDispatcherInterface $eventDispatcher, + Request $request + ): Response { + if ($this->isCsrfTokenValid('delete'.$entity->getId(), $request->request->get('_token'))) { + $eventDispatcher->dispatch(new EntityManagerEvent($entity), EntityManagerEvent::PRE_DELETE_EVENT); + $nodeRepository->removeFromTree($entity); + $nodeRepository->reorder($entity->getMenu()->getRootNode()); + $eventDispatcher->dispatch(new EntityManagerEvent($entity), EntityManagerEvent::DELETE_EVENT); + + $this->addFlash('success', 'Donnée supprimée.'); + } + + return $this->redirectToRoute('admin_site_tree_navigation', [ + 'navigation' => $entity->getMenu()->getNavigation()->getId(), + ]); + } + + public function getSection(): string + { + return ''; + } + + protected function handlePageAssociation( + string $pageAction, + ?Page $pageEntity, + string $pageType, + Entity $entity, + PageFactory $pageFactory, + PageLocator $pageLocator + ) { + if ('new' === $pageAction) { + $pageConfiguration = $pageLocator->getPage($pageType); + $page = $pageFactory->create($pageType, $entity->getLabel()); + $page->setTemplate($pageConfiguration->getTemplates()[0]['file']); + + $entity->setPage($page); + } elseif ('existing' === $pageAction) { + if ($pageEntity) { + $entity->setPage($pageEntity); + } else { + $this->addFlash('info', 'Aucun changement de page effectué.'); + } + } elseif ('none' === $pageAction) { + $entity->setPage(null); + } + } +} diff --git a/core/Controller/Site/PageAdminController.php b/core/Controller/Site/PageAdminController.php new file mode 100644 index 0000000..e42582e --- /dev/null +++ b/core/Controller/Site/PageAdminController.php @@ -0,0 +1,109 @@ +paginate($page); + + return $this->render('site/page_admin/index.html.twig', [ + 'pager' => $pager, + ]); + } + + /** + * @Route("/new", name="admin_site_page_new") + */ + public function new(EntityFactory $factory, EntityManager $entityManager): Response + { + // $entity = $factory->create(FooPage::class); + $entity = $factory->create(SimplePage::class); + $entity->setName('Page de test '.mt_rand()); + + $entityManager->create($entity); + + $this->addFlash('success', 'Donnée enregistrée.'); + + return $this->redirectToRoute('admin_site_page_edit', [ + 'entity' => $entity->getId(), + ]); + } + + /** + * @Route("/edit/{entity}", name="admin_site_page_edit") + */ + public function edit( + int $entity, + EntityFactory $factory, + EntityManager $entityManager, + RepositoryQuery $repositoryQuery, + PageLocator $pageLocator, + Request $request + ): Response { + $entity = $repositoryQuery->filterById($entity)->findOne(); + $form = $this->createForm(EntityType::class, $entity, [ + 'pageConfiguration' => $pageLocator->getPage(get_class($entity)), + ]); + + if ($request->isMethod('POST')) { + $form->handleRequest($request); + + if ($form->isValid()) { + $entityManager->update($entity); + + $this->addFlash('success', 'Donnée enregistrée.'); + + return $this->redirectToRoute('admin_site_page_edit', [ + 'entity' => $entity->getId(), + ]); + } + + $this->addFlash('warning', 'Le formulaire est invalide.'); + } + + return $this->render('site/page_admin/edit.html.twig', [ + 'form' => $form->createView(), + 'entity' => $entity, + ]); + } + + /** + * @Route("/delete/{entity}", name="admin_site_page_delete", methods={"DELETE"}) + */ + public function delete(Entity $entity, EntityManager $entityManager, Request $request): Response + { + if ($this->isCsrfTokenValid('delete'.$entity->getId(), $request->request->get('_token'))) { + $entityManager->delete($entity); + + $this->addFlash('success', 'Données supprimée..'); + } + + return $this->redirectToRoute('admin_site_page_index'); + } + + public function getSection(): string + { + return 'site_page'; + } +} diff --git a/core/Controller/Site/PageController.php b/core/Controller/Site/PageController.php new file mode 100644 index 0000000..7100869 --- /dev/null +++ b/core/Controller/Site/PageController.php @@ -0,0 +1,25 @@ +getPage()) { + throw $this->createNotFoundException(); + } + + return $this->render($siteRequest->getPage()->getTemplate(), [ + '_node' => $siteRequest->getNode(), + '_page' => $siteRequest->getPage(), + '_menu' => $siteRequest->getMenu(), + '_navigation' => $siteRequest->getNavigation(), + ]); + } +} diff --git a/core/Controller/Site/TreeAdminController.php b/core/Controller/Site/TreeAdminController.php new file mode 100644 index 0000000..6700592 --- /dev/null +++ b/core/Controller/Site/TreeAdminController.php @@ -0,0 +1,66 @@ +create()->findOne(); + + if (null === $navigation) { + $this->addFlash('warning', 'Vous devez ajouter une navigation.'); + + return $this->redirectToRoute('admin_site_navigation_new'); + } + + return $this->redirectToRoute('admin_site_tree_navigation', [ + 'navigation' => $navigation->getId(), + ]); + } + + /** + * @Route("/navigation/{navigation}", name="admin_site_tree_navigation") + */ + public function navigation( + Navigation $navigation, + NavigationRepositoryQuery $navigationQuery, + MenuFactory $menuFactory + ): Response { + $navigations = $navigationQuery->create()->find(); + + $forms = [ + 'menu' => $this->createForm(MenuType::class, $menuFactory->create())->createView(), + 'menus' => [], + ]; + + foreach ($navigation->getMenus() as $menu) { + $forms['menus'][$menu->getId()] = $this->createForm(MenuType::class, $menu)->createView(); + } + + return $this->render('site/tree_admin/navigation.html.twig', [ + 'navigation' => $navigation, + 'navigations' => $navigations, + 'forms' => $forms, + ]); + } + + public function getSection(): string + { + return 'site_tree'; + } +} diff --git a/core/Controller/User/UserAdminController.php b/core/Controller/User/UserAdminController.php new file mode 100644 index 0000000..b5cbb08 --- /dev/null +++ b/core/Controller/User/UserAdminController.php @@ -0,0 +1,148 @@ +paginate($page); + + return $this->render('user/user_admin/index.html.twig', [ + 'pager' => $pager, + ]); + } + + /** + * @Route("/new", name="admin_user_new") + */ + public function new( + EntityFactory $factory, + EntityManager $entityManager, + UserPasswordEncoderInterface $encoder, + Request $request + ): Response { + $entity = $factory->create($this->getUser()); + $form = $this->createForm(EntityType::class, $entity); + + if ($request->isMethod('POST')) { + $form->handleRequest($request); + + if ($form->isValid()) { + $entityManager->create($entity); + $this->addFlash('success', 'Donnée enregistrée.'); + + return $this->redirectToRoute('admin_user_edit', [ + 'entity' => $entity->getId(), + ]); + } + $this->addFlash('warning', 'Le formulaire est invalide.'); + } + + return $this->render('user/user_admin/new.html.twig', [ + 'form' => $form->createView(), + 'entity' => $entity, + ]); + } + + /** + * @Route("/edit/{entity}", name="admin_user_edit") + */ + public function edit(Entity $entity, EntityManager $entityManager, Request $request): Response + { + $form = $this->createForm(EntityType::class, $entity); + + if ($request->isMethod('POST')) { + $form->handleRequest($request); + + if ($form->isValid()) { + $entityManager->update($entity); + $this->addFlash('success', 'Donnée enregistrée.'); + + return $this->redirectToRoute('admin_user_edit', [ + 'entity' => $entity->getId(), + ]); + } + $this->addFlash('warning', 'Le formulaire est invalide.'); + } + + return $this->render('user/user_admin/edit.html.twig', [ + 'form' => $form->createView(), + 'entity' => $entity, + ]); + } + + /** + * @Route("/show/{entity}", name="admin_user_show") + */ + public function show(Entity $entity): Response + { + return $this->render('user/user_admin/show.html.twig', [ + 'entity' => $entity, + ]); + } + + /** + * @Route("/resetting_request/{entity}", name="admin_user_resetting_request", methods={"POST"}) + */ + public function requestResetting( + Entity $entity, + EntityManager $entityManager, + TokenGeneratorInterface $tokenGenerator, + EventDispatcherInterface $eventDispatcher, + Request $request + ): Response { + if ($this->isCsrfTokenValid('resetting_request'.$entity->getId(), $request->request->get('_token'))) { + $entity->setConfirmationToken($tokenGenerator->generateToken()); + $entity->setPasswordRequestedAt(new \DateTime('now')); + + $entityManager->update($entity); + $eventDispatcher->dispatch(new PasswordRequestEvent($entity), PasswordRequestEvent::EVENT); + + $this->addFlash('success', 'Demande envoyée.'); + } + + return $this->redirectToRoute('admin_user_edit', [ + 'entity' => $entity->getId(), + ]); + } + + /** + * @Route("/delete/{entity}", name="admin_user_delete", methods={"DELETE"}) + */ + public function delete(Entity $entity, EntityManager $entityManager, Request $request): Response + { + if ($this->isCsrfTokenValid('delete'.$entity->getId(), $request->request->get('_token'))) { + $entityManager->delete($entity); + + $this->addFlash('success', 'Données supprimée..'); + } + + return $this->redirectToRoute('admin_user_index'); + } + + public function getSection(): string + { + return 'user'; + } +} diff --git a/core/DependencyInjection/AppExtension.php b/core/DependencyInjection/AppExtension.php new file mode 100644 index 0000000..dea734b --- /dev/null +++ b/core/DependencyInjection/AppExtension.php @@ -0,0 +1,28 @@ +getConfiguration($configs, $container); + $config = $this->processConfiguration($configuration, $configs); + + $container->setParameter('app', $config); + } + + /** + * {@inheritdoc} + */ + public function getConfiguration(array $configs, ContainerBuilder $container) + { + return new Configuration(); + } +} diff --git a/core/DependencyInjection/Configuration.php b/core/DependencyInjection/Configuration.php new file mode 100644 index 0000000..1cbcd2e --- /dev/null +++ b/core/DependencyInjection/Configuration.php @@ -0,0 +1,44 @@ +getRootNode() + ->children() + ->arrayNode('site') + ->children() + ->arrayNode('pages') + ->prototype('array') + ->children() + ->scalarNode('name') + ->isRequired() + ->cannotBeEmpty() + ->end() + ->arrayNode('templates') + ->prototype('array') + ->children() + ->scalarNode('name') + ->cannotBeEmpty() + ->end() + ->scalarNode('file') + ->cannotBeEmpty() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end() + ->end(); + + return $treeBuilder; + } +} diff --git a/core/Doctrine/Timestampable.php b/core/Doctrine/Timestampable.php new file mode 100644 index 0000000..41ff4b9 --- /dev/null +++ b/core/Doctrine/Timestampable.php @@ -0,0 +1,59 @@ +createdAt = new \DateTime(); + $this->updatedAt = new \DateTime(); + } + + /** + * @ORM\PreUpdate + */ + public function onPreUpdate(): void + { + $this->updatedAt = new \DateTime(); + } + + public function setCreatedAt(?\DateTime $createdAt): self + { + $this->createdAt = $createdAt; + + return $this; + } + + public function getCreatedAt(): ?\DateTime + { + return $this->createdAt; + } + + public function setUpdatedAt(?\DateTime $updatedAt): self + { + $this->updatedAt = $updatedAt; + + return $this; + } + + public function getUpdatedAt(): ?\DateTime + { + return $this->updatedAt; + } +} diff --git a/core/Entity/.gitignore b/core/Entity/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/core/Entity/EntityInterface.php b/core/Entity/EntityInterface.php new file mode 100644 index 0000000..0f8ecd3 --- /dev/null +++ b/core/Entity/EntityInterface.php @@ -0,0 +1,7 @@ +nodes = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getLabel(): ?string + { + return $this->label; + } + + public function setLabel(string $label): self + { + $this->label = $label; + + return $this; + } + + public function getCode(): ?string + { + return $this->code; + } + + public function setCode(string $code): self + { + $this->code = $code; + + return $this; + } + + public function getNavigation(): ?Navigation + { + return $this->navigation; + } + + public function setNavigation(?Navigation $navigation): self + { + $this->navigation = $navigation; + + return $this; + } + + /** + * @return Collection|Node[] + */ + public function getNodes(): Collection + { + return $this->nodes; + } + + public function addNode(Node $node): self + { + if (!$this->nodes->contains($node)) { + $this->nodes[] = $node; + $node->setMenu($this); + } + + return $this; + } + + public function removeNode(Node $node): self + { + if ($this->nodes->removeElement($node)) { + // set the owning side to null (unless already changed) + if ($node->getMenu() === $this) { + $node->setMenu(null); + } + } + + return $this; + } + + public function getRootNode(): ?Node + { + return $this->rootNode; + } + + public function setRootNode(?Node $rootNode): self + { + $this->rootNode = $rootNode; + + return $this; + } + + public function getRouteName(): string + { + return $this->getNavigation()->getRouteName().'_'.($this->getCode() ? $this->getCode() : $this->getId()); + } +} diff --git a/core/Entity/Site/Navigation.php b/core/Entity/Site/Navigation.php new file mode 100644 index 0000000..5672629 --- /dev/null +++ b/core/Entity/Site/Navigation.php @@ -0,0 +1,127 @@ +menus = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getLabel(): ?string + { + return $this->label; + } + + public function setLabel(string $label): self + { + $this->label = $label; + + return $this; + } + + public function getCode(): ?string + { + return $this->code; + } + + public function setCode(string $code): self + { + $this->code = $code; + + return $this; + } + + public function getDomain(): ?string + { + return $this->domain; + } + + public function setDomain(string $domain): self + { + $this->domain = $domain; + + return $this; + } + + /** + * @return Collection|Menu[] + */ + public function getMenus(): Collection + { + return $this->menus; + } + + public function addMenu(Menu $menu): self + { + if (!$this->menus->contains($menu)) { + $this->menus[] = $menu; + $menu->setNavigation($this); + } + + return $this; + } + + public function removeMenu(Menu $menu): self + { + if ($this->menus->removeElement($menu)) { + // set the owning side to null (unless already changed) + if ($menu->getNavigation() === $this) { + $menu->setNavigation(null); + } + } + + return $this; + } + + public function getRouteName(): string + { + return $this->getCode() ? $this->getCode() : 'navigation_'.$this->getId(); + } +} diff --git a/core/Entity/Site/Node.php b/core/Entity/Site/Node.php new file mode 100644 index 0000000..9b89de4 --- /dev/null +++ b/core/Entity/Site/Node.php @@ -0,0 +1,308 @@ +children = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getMenu(): ?Menu + { + return $this->menu; + } + + public function setMenu(?Menu $menu): self + { + $this->menu = $menu; + + return $this; + } + + public function getTreeLeft(): ?int + { + return $this->treeLeft; + } + + public function setTreeLeft(int $treeLeft): self + { + $this->treeLeft = $treeLeft; + + return $this; + } + + public function getTreeLevel(): ?int + { + return $this->treeLevel; + } + + public function setTreeLevel(int $treeLevel): self + { + $this->treeLevel = $treeLevel; + + return $this; + } + + public function getTreeRight(): ?int + { + return $this->treeRight; + } + + public function setTreeRight(int $treeRight): self + { + $this->treeRight = $treeRight; + + return $this; + } + + public function getTreeRoot(): ?self + { + return $this->treeRoot; + } + + public function setTreeRoot(?self $treeRoot): self + { + $this->treeRoot = $treeRoot; + + return $this; + } + + public function getParent(): ?self + { + return $this->parent; + } + + public function setParent(?self $parent): self + { + $this->parent = $parent; + + return $this; + } + + /** + * @return Collection|Node[] + */ + public function getChildren(): Collection + { + if (null === $this->children) { + $this->children = new ArrayCollection(); + } + + return $this->children; + } + + public function addChild(Node $child): self + { + if (!$this->children->contains($child)) { + $this->children[] = $child; + $child->setParent($this); + } + + return $this; + } + + public function removeChild(Node $child): self + { + if ($this->children->removeElement($child)) { + // set the owning side to null (unless already changed) + if ($child->getParent() === $this) { + $child->setParent(null); + } + } + + return $this; + } + + public function getAllChildren(): ArrayCollection + { + $children = []; + + $getChildren = function (Node $node) use (&$children, &$getChildren) { + foreach ($node->getChildren() as $nodeChildren) { + $children[] = $nodeChildren; + + $getChildren($nodeChildren); + } + }; + + $getChildren($this); + + usort($children, function ($a, $b) { + return $a->getTreeLeft() < $b->getTreeLeft() ? -1 : 1; + }); + + return new ArrayCollection($children); + } + + public function getLabel(): ?string + { + return $this->label; + } + + public function setLabel(?string $label): self + { + $this->label = $label; + + return $this; + } + + public function getUrl(): ?string + { + return $this->url; + } + + public function setUrl(?string $url): self + { + $this->url = $url; + + return $this; + } + + public function getIsVisible(): ?bool + { + return $this->isVisible; + } + + public function setIsVisible(bool $isVisible): self + { + $this->isVisible = $isVisible; + + return $this; + } + + public function getTreeLabel() + { + $prefix = str_repeat('-', ($this->getTreeLevel() - 1) * 5); + + return trim($prefix.' '.$this->getLabel()); + } + + public function getPage(): ?Page + { + return $this->page; + } + + public function setPage(?Page $page): self + { + $this->page = $page; + + return $this; + } + + public function getRouteName(): string + { + return $this->getMenu()->getRouteName().'_'.($this->getCode() ? $this->getCode() : $this->getId()); + } + + public function getCode(): ?string + { + return $this->code; + } + + public function setCode(?string $code): self + { + $this->code = $code; + + return $this; + } +} diff --git a/core/Entity/Site/Page/Block.php b/core/Entity/Site/Page/Block.php new file mode 100644 index 0000000..a92223c --- /dev/null +++ b/core/Entity/Site/Page/Block.php @@ -0,0 +1,80 @@ +id; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + + public function getValue(): ?string + { + return $this->value; + } + + public function setValue(string $value): self + { + $this->value = $value; + + return $this; + } + + public function getPage(): ?Page + { + return $this->page; + } + + public function setPage(?Page $page): self + { + $this->page = $page; + + return $this; + } +} diff --git a/core/Entity/Site/Page/Page.php b/core/Entity/Site/Page/Page.php new file mode 100644 index 0000000..c0ccc6f --- /dev/null +++ b/core/Entity/Site/Page/Page.php @@ -0,0 +1,248 @@ +blocks = new ArrayCollection(); + $this->nodes = new ArrayCollection(); + } + + public function getId(): ?int + { + return $this->id; + } + + public function getName(): ?string + { + return $this->name; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + + public function getTemplate(): ?string + { + return $this->template; + } + + public function setTemplate(?string $template): self + { + $this->template = $template; + + return $this; + } + + /** + * @return Collection|Block[] + */ + public function getBlocks(): Collection + { + return $this->blocks; + } + + public function addBlock(Block $block): self + { + if (!$this->blocks->contains($block)) { + $this->blocks[] = $block; + $block->setPage($this); + } + + return $this; + } + + public function removeBlock(Block $block): self + { + if ($this->blocks->removeElement($block)) { + // set the owning side to null (unless already changed) + if ($block->getPage() === $this) { + $block->setPage(null); + } + } + + return $this; + } + + public function buildForm(FormBuilderInterface $builder) + { + } + + public function getBlock($name) + { + foreach ($this->getBlocks() as $block) { + if ($block->getName() === $name) { + return $block; + } + } + + $block = new Block(); + $block->setName($name); + $block->setPage($this); + + return $block; + } + + public function setBlock(Block $block): self + { + foreach ($this->blocks->toArray() as $key => $value) { + if ($value->getName() === $block->getName()) { + $this->blocks->remove($key); + $this->blocks->add($block); + + return $this; + } + } + + $this->blocks->add($block); + + return $this; + } + + public function getMetaTitle(): ?string + { + return $this->metaTitle; + } + + public function setMetaTitle(?string $metaTitle): self + { + $this->metaTitle = $metaTitle; + + return $this; + } + + public function getMetaDescrition(): ?string + { + return $this->metaDescrition; + } + + public function setMetaDescrition(?string $metaDescrition): self + { + $this->metaDescrition = $metaDescrition; + + return $this; + } + + public function getOgTitle(): ?string + { + return $this->ogTitle; + } + + public function setOgTitle(?string $ogTitle): self + { + $this->ogTitle = $ogTitle; + + return $this; + } + + public function getOgDescription(): ?string + { + return $this->ogDescription; + } + + public function setOgDescription(?string $ogDescription): self + { + $this->ogDescription = $ogDescription; + + return $this; + } + + /** + * @return Collection|Node[] + */ + public function getNodes(): Collection + { + return $this->nodes; + } + + public function addNode(Node $node): self + { + if (!$this->nodes->contains($node)) { + $this->nodes[] = $node; + $node->setPage($this); + } + + return $this; + } + + public function removeNode(Node $node): self + { + if ($this->nodes->removeElement($node)) { + // set the owning side to null (unless already changed) + if ($node->getPage() === $this) { + $node->setPage(null); + } + } + + return $this; + } +} diff --git a/core/Event/Account/PasswordRequestEvent.php b/core/Event/Account/PasswordRequestEvent.php new file mode 100644 index 0000000..bede88d --- /dev/null +++ b/core/Event/Account/PasswordRequestEvent.php @@ -0,0 +1,28 @@ + + */ +class PasswordRequestEvent extends Event +{ + const EVENT = 'account_event.password_request'; + + protected User $user; + + public function __construct(User $user) + { + $this->user = $user; + } + + public function getUser(): USer + { + return $this->user; + } +} diff --git a/core/Event/EntityManager/EntityManagerEvent.php b/core/Event/EntityManager/EntityManagerEvent.php new file mode 100644 index 0000000..ee69320 --- /dev/null +++ b/core/Event/EntityManager/EntityManagerEvent.php @@ -0,0 +1,33 @@ + + */ +class EntityManagerEvent extends Event +{ + const CREATE_EVENT = 'entity_manager_event.create'; + const UPDATE_EVENT = 'entity_manager_event.update'; + const DELETE_EVENT = 'entity_manager_event.delete'; + const PRE_CREATE_EVENT = 'entity_manager_event.pre_create'; + const PRE_UPDATE_EVENT = 'entity_manager_event.pre_update'; + const PRE_DELETE_EVENT = 'entity_manager_event.pre_delete'; + + protected EntityInterface $entity; + + public function __construct(EntityInterface $entity) + { + $this->entity = $entity; + } + + public function getEntity(): EntityInterface + { + return $this->entity; + } +} diff --git a/core/EventSuscriber/Account/PasswordRequestEventSubscriber.php b/core/EventSuscriber/Account/PasswordRequestEventSubscriber.php new file mode 100644 index 0000000..fdd187e --- /dev/null +++ b/core/EventSuscriber/Account/PasswordRequestEventSubscriber.php @@ -0,0 +1,50 @@ + + */ +class PasswordRequestEventSubscriber implements EventSubscriberInterface +{ + protected MailNotifier $notifier; + protected UrlGeneratorInterface $urlGenerator; + + public function __construct(MailNotifier $notifier, UrlGeneratorInterface $urlGenerator) + { + $this->notifier = $notifier; + $this->urlGenerator = $urlGenerator; + } + + public static function getSubscribedEvents() + { + return [ + PasswordRequestEvent::EVENT => 'onRequest', + ]; + } + + public function onRequest(PasswordRequestEvent $event) + { + $this->notifier + ->setFrom('system@tinternet.net') + ->setSubject('[Tinternet & cie] Mot de passe perdu') + ->addRecipient($event->getUser()->getEmail()) + ->notify('resetting_request', [ + 'reseting_update_link' => $this->urlGenerator->generate( + 'auth_resetting_update', + [ + 'token' => $event->getUser()->getConfirmationToken(), + ], + UrlGeneratorInterface::ABSOLUTE_URL + ), + ]) + ; + } +} diff --git a/core/EventSuscriber/EntityManagerEventSubscriber.php b/core/EventSuscriber/EntityManagerEventSubscriber.php new file mode 100644 index 0000000..586fbf4 --- /dev/null +++ b/core/EventSuscriber/EntityManagerEventSubscriber.php @@ -0,0 +1,50 @@ + + */ +abstract class EntityManagerEventSubscriber implements EventSubscriberInterface +{ + public static function getSubscribedEvents() + { + return [ + EntityManagerEvent::CREATE_EVENT => 'onCreate', + EntityManagerEvent::UPDATE_EVENT => 'onUpdate', + EntityManagerEvent::DELETE_EVENT => 'onDelete', + EntityManagerEvent::PRE_CREATE_EVENT => 'onPreCreate', + EntityManagerEvent::PRE_UPDATE_EVENT => 'onPreUpdate', + EntityManagerEvent::PRE_DELETE_EVENT => 'onPreDelete', + ]; + } + + public function onCreate(EntityManagerEvent $event) + { + } + + public function onUpdate(EntityManagerEvent $event) + { + } + + public function onDelete(EntityManagerEvent $event) + { + } + + public function onPreCreate(EntityManagerEvent $event) + { + } + + public function onPreUpdate(EntityManagerEvent $event) + { + } + + public function onPreDelete(EntityManagerEvent $event) + { + } +} diff --git a/core/EventSuscriber/Site/MenuEventSubscriber.php b/core/EventSuscriber/Site/MenuEventSubscriber.php new file mode 100644 index 0000000..205165f --- /dev/null +++ b/core/EventSuscriber/Site/MenuEventSubscriber.php @@ -0,0 +1,92 @@ + + */ +class MenuEventSubscriber extends EntityManagerEventSubscriber +{ + protected NodeFactory $nodeFactory; + protected NodeRepository $nodeRepository; + protected EntityManager $entityManager; + protected CodeSlugify $slugify; + + public function __construct( + NodeFactory $nodeFactory, + NodeRepository $nodeRepository, + EntityManager $entityManager, + CodeSlugify $slugify + ) { + $this->nodeFactory = $nodeFactory; + $this->nodeRepository = $nodeRepository; + $this->entityManager = $entityManager; + $this->slugify = $slugify; + } + + public function support(EntityInterface $entity) + { + return $entity instanceof Menu; + } + + public function onPreUpdate(EntityManagerEvent $event) + { + if (!$this->support($event->getEntity())) { + return; + } + + $menu = $event->getEntity(); + $menu->setCode($this->slugify->slugify($menu->getCode())); + } + + public function onCreate(EntityManagerEvent $event) + { + if (!$this->support($event->getEntity())) { + return; + } + + $menu = $event->getEntity(); + + if (0 !== count($menu->getNodes())) { + return; + } + + $rootNode = $this->nodeFactory->create($menu); + $childNode = $this->nodeFactory->create($menu, '/'); + $childNode + ->setParent($rootNode) + ->setLabel('Premier élément') + ; + + $menu->setRootNode($rootNode); + + $this->entityManager->getEntityManager()->persist($rootNode); + $this->entityManager->getEntityManager()->persist($childNode); + + $this->entityManager->getEntityManager()->persist($menu); + $this->entityManager->flush(); + + $this->nodeRepository->persistAsFirstChild($childNode, $rootNode); + } + + public function onUpdate(EntityManagerEvent $event) + { + return $this->onCreate($event); + } + + public function onPreCreate(EntityManagerEvent $event) + { + return $this->onPreUpdate($event); + } +} diff --git a/core/EventSuscriber/Site/NavigationEventSubscriber.php b/core/EventSuscriber/Site/NavigationEventSubscriber.php new file mode 100644 index 0000000..f36c958 --- /dev/null +++ b/core/EventSuscriber/Site/NavigationEventSubscriber.php @@ -0,0 +1,46 @@ + + */ +class NavigationEventSubscriber extends EntityManagerEventSubscriber +{ + public function __construct( + EntityManager $entityManager, + CodeSlugify $slugify + ) { + $this->entityManager = $entityManager; + $this->slugify = $slugify; + } + + public function support(EntityInterface $entity) + { + return $entity instanceof Navigation; + } + + public function onPreUpdate(EntityManagerEvent $event) + { + if (!$this->support($event->getEntity())) { + return; + } + + $menu = $event->getEntity(); + $menu->setCode($this->slugify->slugify($menu->getCode())); + } + + public function onPreCreate(EntityManagerEvent $event) + { + return $this->onPreUpdate($event); + } +} diff --git a/core/EventSuscriber/Site/NodeEventSubscriber.php b/core/EventSuscriber/Site/NodeEventSubscriber.php new file mode 100644 index 0000000..95c2844 --- /dev/null +++ b/core/EventSuscriber/Site/NodeEventSubscriber.php @@ -0,0 +1,109 @@ + + */ +class NodeEventSubscriber extends EntityManagerEventSubscriber +{ + protected NodeFactory $nodeFactory; + protected EntityManager $entityManager; + protected KernelInterface $kernel; + protected Slugify $slugify; + + public function __construct( + NodeFactory $nodeFactory, + NodeRepository $nodeRepository, + EntityManager $entityManager, + Slugify $slugify + ) { + $this->nodeFactory = $nodeFactory; + $this->nodeRepository = $nodeRepository; + $this->entityManager = $entityManager; + $this->slugify = $slugify; + } + + public function support(EntityInterface $entity) + { + return $entity instanceof Node; + } + + public function onPreUpdate(EntityManagerEvent $event) + { + if (!$this->support($event->getEntity())) { + return; + } + + $node = $event->getEntity(); + + if ($node->getUrl()) { + $generatedUrl = $node->getUrl(); + } else { + $path = []; + $parent = $node->getParent(); + + if ($parent && $parent->getUrl()) { + $pPath = trim($parent->getUrl(), '/'); + + if ($pPath) { + $path[] = $pPath; + } + } + + $path[] = $this->slugify->slugify($node->getLabel()); + + $generatedUrl = '/'.implode('/', $path); + } + + $urlExists = $this->nodeRepository->urlExists($generatedUrl, $node); + + if ($urlExists) { + $number = 1; + + while ($this->nodeRepository->urlExists($generatedUrl.'-'.$number, $node)) { + ++$number; + } + + $generatedUrl = $generatedUrl.'-'.$number; + } + + $node->setUrl($generatedUrl); + } + + public function onDelete(EntityManagerEvent $event) + { + if (!$this->support($event->getEntity())) { + return; + } + + $menu = $event->getEntity()->getMenu(); + $rootNode = $menu->getRootNode(); + + if (0 !== count($rootNode->getChildren())) { + return; + } + + $childNode = $this->nodeFactory->create($menu); + $childNode + ->setParent($rootNode) + ->setLabel('Premier élément') + ; + + $this->entityManager->update($rootNode, false); + $this->entityManager->create($childNode, false); + $this->nodeRepository->persistAsFirstChild($childNode, $rootNode); + } +} diff --git a/core/EventSuscriber/Site/SiteEventSubscriber.php b/core/EventSuscriber/Site/SiteEventSubscriber.php new file mode 100644 index 0000000..a4cc73b --- /dev/null +++ b/core/EventSuscriber/Site/SiteEventSubscriber.php @@ -0,0 +1,70 @@ + + */ +class SiteEventSubscriber extends EntityManagerEventSubscriber +{ + protected KernelInterface $kernel; + + public function __construct(KernelInterface $kernel) { + $this->kernel = $kernel; + } + + public function support(EntityInterface $entity) + { + return $entity instanceof Node || $entity instanceof Menu || $entity instanceof Navigation; + } + + protected function cleanCache() + { + $application = new Application($this->kernel); + $application->setAutoExit(false); + + $input = new ArrayInput([ + 'command' => 'cache:clear', + ]); + + $output = new BufferedOutput(); + $application->run($input, $output); + } + + public function onUpdate(EntityManagerEvent $event) + { + if (!$this->support($event->getEntity())) { + return; + } + + $this->cleanCache(); + } + + public function onCreate(EntityManagerEvent $event) + { + return $this->onUpdate($event); + } + + public function onDelete(EntityManagerEvent $event) + { + return $this->onUpdate($event); + } + +} diff --git a/core/Factory/Site/MenuFactory.php b/core/Factory/Site/MenuFactory.php new file mode 100644 index 0000000..c388e8c --- /dev/null +++ b/core/Factory/Site/MenuFactory.php @@ -0,0 +1,25 @@ + + */ +class MenuFactory +{ + public function create(?Navigation $navigation = null): Menu + { + $entity = new Menu(); + + if (null !== $navigation) { + $entity->setNavigation($navigation); + } + + return $entity; + } +} diff --git a/core/Factory/Site/NavigationFactory.php b/core/Factory/Site/NavigationFactory.php new file mode 100644 index 0000000..52b2d66 --- /dev/null +++ b/core/Factory/Site/NavigationFactory.php @@ -0,0 +1,18 @@ + + */ +class NavigationFactory +{ + public function create(): Navigation + { + return new Navigation(); + } +} diff --git a/core/Factory/Site/NodeFactory.php b/core/Factory/Site/NodeFactory.php new file mode 100644 index 0000000..d9aa180 --- /dev/null +++ b/core/Factory/Site/NodeFactory.php @@ -0,0 +1,30 @@ + + */ +class NodeFactory +{ + public function create(?Menu $menu = null, string $url = null): Node + { + $entity = new Node(); + + if (null !== $menu) { + $entity->setMenu($menu); + } + + if (null !== $url) { + $entity->setUrl($url); + } + + + return $entity; + } +} diff --git a/core/Factory/Site/Page/PageFactory.php b/core/Factory/Site/Page/PageFactory.php new file mode 100644 index 0000000..2ecdd07 --- /dev/null +++ b/core/Factory/Site/Page/PageFactory.php @@ -0,0 +1,21 @@ + + */ +class PageFactory +{ + public function create(string $className, string $name): Page + { + $entity = new $className(); + $entity->setName($name); + + return $entity; + } +} diff --git a/core/Factory/UserFactory.php b/core/Factory/UserFactory.php new file mode 100644 index 0000000..c5e7905 --- /dev/null +++ b/core/Factory/UserFactory.php @@ -0,0 +1,36 @@ + + */ +class UserFactory +{ + protected TokenGeneratorInterface $tokenGenerator; + protected UserPasswordEncoderInterface $encoder; + + public function __construct(TokenGeneratorInterface $tokenGenerator, UserPasswordEncoderInterface $encoder) + { + $this->tokenGenerator = $tokenGenerator; + $this->encoder = $encoder; + } + + public function create(): User + { + $entity = new User(); + + $entity->setPassword($this->encoder->encodePassword( + $entity, + $this->tokenGenerator->generateToken() + )); + + return $entity; + } +} diff --git a/core/Form/FileUploadHandler.php b/core/Form/FileUploadHandler.php new file mode 100644 index 0000000..1378b32 --- /dev/null +++ b/core/Form/FileUploadHandler.php @@ -0,0 +1,28 @@ + + */ +class FileUploadHandler +{ + public function handleForm(?UploadedFile $uploadedFile, string $path, callable $afterUploadCallback): void + { + if (null === $uploadedFile) { + return; + } + + $originalFilename = pathinfo($uploadedFile->getClientOriginalName(), PATHINFO_FILENAME); + $safeFilename = transliterator_transliterate('Any-Latin; Latin-ASCII; [^A-Za-z0-9_] remove; Lower()', $originalFilename); + $filename = date('Ymd-his').$safeFilename.'.'.$uploadedFile->guessExtension(); + + $uploadedFile->move($path, $filename); + + $afterUploadCallback($filename); + } +} diff --git a/core/Form/Site/MenuType.php b/core/Form/Site/MenuType.php new file mode 100644 index 0000000..ee65957 --- /dev/null +++ b/core/Form/Site/MenuType.php @@ -0,0 +1,51 @@ +add( + 'label', + TextType::class, + [ + 'label' => 'Libellé', + 'required' => true, + 'attr' => [ + ], + 'constraints' => [ + new NotBlank(), + ], + ] + ); + + $builder->add( + 'code', + TextType::class, + [ + 'label' => 'Code', + 'required' => true, + 'attr' => [ + ], + 'constraints' => [ + new NotBlank(), + ], + ] + ); + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => Menu::class, + ]); + } +} diff --git a/core/Form/Site/NavigationType.php b/core/Form/Site/NavigationType.php new file mode 100644 index 0000000..ac26b6c --- /dev/null +++ b/core/Form/Site/NavigationType.php @@ -0,0 +1,65 @@ +add( + 'label', + TextType::class, + [ + 'label' => 'Libellé', + 'required' => true, + 'attr' => [ + ], + 'constraints' => [ + new NotBlank(), + ], + ] + ); + + $builder->add( + 'code', + TextType::class, + [ + 'label' => 'Code', + 'required' => true, + 'attr' => [ + ], + 'constraints' => [ + new NotBlank(), + ], + ] + ); + + $builder->add( + 'domain', + TextType::class, + [ + 'label' => 'Nom de domaine', + 'required' => true, + 'attr' => [ + ], + 'constraints' => [ + new NotBlank(), + ], + ] + ); + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => Navigation::class, + ]); + } +} diff --git a/core/Form/Site/NodeMoveType.php b/core/Form/Site/NodeMoveType.php new file mode 100644 index 0000000..669b9c2 --- /dev/null +++ b/core/Form/Site/NodeMoveType.php @@ -0,0 +1,62 @@ +add( + 'position', + ChoiceType::class, + [ + 'label' => 'Position', + 'required' => true, + 'choices' => [ + 'Après' => 'after', + 'Avant' => 'before', + 'En dessous' => 'above', + ], + 'attr' => [ + ], + 'constraints' => [ + new NotBlank(), + ], + ] + ); + + $builder->add( + 'node', + EntityType::class, + [ + 'label' => 'Élement de référence', + 'class' => Node::class, + 'choices' => call_user_func(function () use ($options) { + return $options['menu']->getRootNode()->getAllChildren(); + }), + 'choice_label' => 'treeLabel', + 'attr' => [ + ], + 'constraints' => [ + new NotBlank(), + ], + ] + ); + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => null, + 'menu' => null, + ]); + } +} diff --git a/core/Form/Site/NodeType.php b/core/Form/Site/NodeType.php new file mode 100644 index 0000000..6890928 --- /dev/null +++ b/core/Form/Site/NodeType.php @@ -0,0 +1,158 @@ +add( + 'label', + TextType::class, + [ + 'label' => 'Libellé', + 'required' => true, + 'attr' => [ + ], + 'constraints' => [ + new NotBlank(), + ], + ] + ); + + $builder->add( + 'url', + TextType::class, + [ + 'label' => 'URL', + 'required' => false, + 'help' => 'Laisser vide pour une génération automatique', + 'attr' => [ + ], + 'constraints' => [ + ], + ] + ); + + $builder->add( + 'code', + TextType::class, + [ + 'label' => 'Code', + 'required' => false, + 'help' => 'Sans espace, en minusule, sans caractère spécial', + 'attr' => [ + ], + 'constraints' => [ + ], + ] + ); + + $actions = [ + 'Nouvelle page' => 'new', + 'Associer à une page existante' => 'existing', + 'Aucune page' => 'none', + ]; + + if ($builder->getData()->getId()) { + $actions['Garder la configuration actuelle'] = 'keep'; + } + + $builder->add( + 'pageAction', + ChoiceType::class, + [ + 'label' => false, + 'required' => true, + 'expanded' => true, + 'mapped' => false, + 'choices' => $actions, + 'constraints' => [ + new NotBlank(), + ], + ] + ); + + $builder->add( + 'pageType', + ChoiceType::class, + [ + 'label' => false, + 'required' => true, + 'mapped' => false, + 'choices' => call_user_func(function () use ($options) { + $choices = []; + + foreach ($options['pages'] as $page) { + $choices[$page->getName()] = $page->getClassName(); + } + + return $choices; + }), + 'constraints' => [ + new NotBlank(), + ], + ] + ); + + $builder->add( + 'pageEntity', + EntityType::class, + [ + 'label' => false, + 'required' => true, + 'mapped' => false, + 'class' => Page::class, + 'choice_label' => 'name', + 'query_builder' => function (EntityRepository $repo) { + return $repo->createQueryBuilder('p') + ->orderBy('p.name', 'ASC') + ; + }, + 'constraints' => [ + ], + ] + ); + + if (null === $builder->getData()->getId()) { + $builder->add( + 'position', + ChoiceType::class, + [ + 'label' => 'Position', + 'required' => true, + 'mapped' => false, + 'choices' => [ + 'Après' => 'after', + 'Avant' => 'before', + 'En dessous' => 'above', + ], + 'attr' => [ + ], + 'constraints' => [ + new NotBlank(), + ], + ] + ); + } + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => Node::class, + 'pages' => [], + ]); + } +} diff --git a/core/Form/Site/Page/PageType.php b/core/Form/Site/Page/PageType.php new file mode 100644 index 0000000..7e40fd8 --- /dev/null +++ b/core/Form/Site/Page/PageType.php @@ -0,0 +1,116 @@ +add( + 'name', + TextType::class, + [ + 'label' => 'Nom', + 'required' => true, + 'attr' => [ + ], + 'constraints' => [ + new NotBlank(), + ], + ] + ); + + $builder->add( + 'metaTitle', + TextType::class, + [ + 'label' => 'Titre', + 'required' => false, + 'attr' => [ + ], + 'constraints' => [ + ], + ] + ); + + $builder->add( + 'metaDescrition', + TextType::class, + [ + 'label' => 'Description', + 'required' => false, + 'attr' => [ + ], + 'constraints' => [ + ], + ] + ); + + $builder->add( + 'ogTitle', + TextType::class, + [ + 'label' => 'Titre', + 'required' => false, + 'attr' => [ + ], + 'constraints' => [ + ], + ] + ); + + $builder->add( + 'ogDescription', + TextType::class, + [ + 'label' => 'Description', + 'required' => false, + 'attr' => [ + ], + 'constraints' => [ + ], + ] + ); + + $builder->add( + 'template', + ChoiceType::class, + [ + 'label' => 'Rendu', + 'required' => true, + 'choices' => call_user_func(function () use ($options) { + $choices = []; + + foreach ($options['pageConfiguration']->getTemplates() as $template) { + $choices[$template['name']] = $template['file']; + } + + return $choices; + }), + 'attr' => [ + ], + 'constraints' => [ + new NotBlank(), + ], + ] + ); + + $builder->getData()->buildForm($builder); + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => Page::class, + 'pageConfiguration' => null, + ]); + } +} diff --git a/core/Form/Site/Page/TextBlockType.php b/core/Form/Site/Page/TextBlockType.php new file mode 100644 index 0000000..a642499 --- /dev/null +++ b/core/Form/Site/Page/TextBlockType.php @@ -0,0 +1,32 @@ +add( + 'value', + TextType::class, + array_merge([ + 'required' => false, + 'label' => false, + ], $options['options']), + ); + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => Block::class, + 'options' => [], + ]); + } +} diff --git a/core/Form/Site/Page/TextareaBlockType.php b/core/Form/Site/Page/TextareaBlockType.php new file mode 100644 index 0000000..87ea68a --- /dev/null +++ b/core/Form/Site/Page/TextareaBlockType.php @@ -0,0 +1,21 @@ +add( + 'value', + TextareaType::class, + array_merge([ + 'required' => false, + 'label' => false, + ], $options['options']), + ); + } +} diff --git a/core/Form/UserType.php b/core/Form/UserType.php new file mode 100644 index 0000000..3e80c0e --- /dev/null +++ b/core/Form/UserType.php @@ -0,0 +1,91 @@ +add( + 'email', + EmailType::class, + [ + 'label' => 'E-mail', + 'required' => true, + 'attr' => [ + ], + 'constraints' => [ + new Email(), + ], + ] + ); + + $builder->add( + 'displayName', + TextType::class, + [ + 'label' => 'Nom complet', + 'required' => true, + 'attr' => [ + ], + 'constraints' => [ + ], + ] + ); + + $builder->add( + 'displayName', + TextType::class, + [ + 'label' => 'Nom complet', + 'required' => true, + 'attr' => [ + ], + 'constraints' => [ + ], + ] + ); + + $builder->add( + 'isAdmin', + CheckboxType::class, + [ + 'label' => 'Administrateur⋅trice', + 'required' => false, + 'attr' => [ + ], + 'constraints' => [ + ], + ] + ); + + $builder->add( + 'isWriter', + CheckboxType::class, + [ + 'label' => 'Rédacteur⋅trice', + 'required' => false, + 'attr' => [ + ], + 'constraints' => [ + ], + ] + ); + } + + public function configureOptions(OptionsResolver $resolver) + { + $resolver->setDefaults([ + 'data_class' => User::class, + ]); + } +} diff --git a/core/Manager/EntityManager.php b/core/Manager/EntityManager.php new file mode 100644 index 0000000..c4eb80e --- /dev/null +++ b/core/Manager/EntityManager.php @@ -0,0 +1,98 @@ + + */ +class EntityManager +{ + protected EventDispatcherInterface $eventDispatcher; + + protected DoctrineEntityManager $entityManager; + + public function __construct(EventDispatcherInterface $eventDispatcher, EntityManagerInterface $entityManager) + { + $this->eventDispatcher = $eventDispatcher; + $this->entityManager = $entityManager; + } + + public function create(EntityInterface $entity, bool $dispatchEvent = true): self + { + if ($dispatchEvent) { + $this->eventDispatcher->dispatch(new EntityManagerEvent($entity), EntityManagerEvent::PRE_CREATE_EVENT); + } + + $this->persist($entity); + + if ($dispatchEvent) { + $this->eventDispatcher->dispatch(new EntityManagerEvent($entity), EntityManagerEvent::CREATE_EVENT); + } + + return $this; + } + + public function update(EntityInterface $entity, bool $dispatchEvent = true): self + { + if ($dispatchEvent) { + $this->eventDispatcher->dispatch(new EntityManagerEvent($entity), EntityManagerEvent::PRE_UPDATE_EVENT); + } + + $this->persist($entity); + + if ($dispatchEvent) { + $this->eventDispatcher->dispatch(new EntityManagerEvent($entity), EntityManagerEvent::UPDATE_EVENT); + } + + return $this; + } + + public function delete(EntityInterface $entity, bool $dispatchEvent = true): self + { + if ($dispatchEvent) { + $this->eventDispatcher->dispatch(new EntityManagerEvent($entity), EntityManagerEvent::PRE_DELETE_EVENT); + } + + $this->entityManager->remove($entity); + $this->flush(); + + if ($dispatchEvent) { + $this->eventDispatcher->dispatch(new EntityManagerEvent($entity), EntityManagerEvent::DELETE_EVENT); + } + + return $this; + } + + public function flush(): self + { + $this->entityManager->flush(); + + return $this; + } + + public function clear(): self + { + $this->entityManager->clear(); + + return $this; + } + + public function getEntityManager(): EntityManagerInterface + { + return $this->entityManager; + } + + protected function persist(EntityInterface $entity) + { + $this->entityManager->persist($entity); + $this->flush(); + } +} diff --git a/core/Notification/MailNotifier.php b/core/Notification/MailNotifier.php new file mode 100644 index 0000000..d830c53 --- /dev/null +++ b/core/Notification/MailNotifier.php @@ -0,0 +1,338 @@ + + */ +class MailNotifier +{ + /** + * @var Swift_Mailer + */ + protected $mailer; + + /** + * @var array + */ + protected $attachments = []; + + /** + * @var array + */ + protected $recipients = []; + + /** + * @var array + */ + protected $bccRecipients = []; + + /** + * @var string + */ + protected $subject; + + /** + * @var string + */ + protected $from; + + /** + * @var string + */ + protected $replyTo; + + /** + * Constructor. + * + * @param BasicNotifier $basicNotifier + * @param Swift_Mailer $mail + */ + public function __construct(TwigEnvironment $twig, Swift_Mailer $mailer) + { + $this->mailer = $mailer; + $this->twig = $twig; + } + + /** + * @return EmailNotifier + */ + public function setMailer(Swift_Mailer $mailer): self + { + $this->mailer = $mailer; + + return $this; + } + + public function getMailer(): Swift_Mailer + { + return $this->mailer; + } + + /** + * @return EmailNotifier + */ + public function setRecipients(array $recipients): self + { + $this->recipients = $recipients; + + return $this; + } + + public function getRecipients(): array + { + return $this->recipients; + } + + /** + * @return EmailNotifier + */ + public function setBccRecipients(array $bccRecipients): self + { + $this->bccRecipients = $bccRecipients; + + return $this; + } + + public function getBccRecipients(): array + { + return $this->bccRecipients; + } + + /** + * @param string $subject + * + * @return EmailNotifier + */ + public function setSubject(?string $subject): self + { + $this->subject = $subject; + + return $this; + } + + public function getSubject(): string + { + return $this->subject; + } + + /** + * @param mixed $from + * + * @return EmailNotifier + */ + public function setFrom($from): self + { + $this->from = $from; + + return $this; + } + + /** + * @return mixed + */ + public function getFrom(): ?string + { + return $this->from; + } + + /** + * Set the value of "replyTo". + * + * @param string $replyTo + * + * @return EmailNotifier + */ + public function setReplyTo($replyTo): self + { + $this->replyTo = $replyTo; + + return $this; + } + + /* + * Get the value of "replyTo". + * + * @return string + */ + public function getReplyTo(): ?string + { + return $this->replyTo; + } + + /** + * @return EmailNotifier + */ + public function setAttachments(array $attachments): self + { + $this->attachments = $attachments; + + return $this; + } + + public function getAttachments(): array + { + return $this->attachments; + } + + /** + * @return EmailNotifier + */ + public function addRecipient(string $email, bool $isBcc = false): self + { + if (!filter_var($email, FILTER_VALIDATE_EMAIL)) { + throw new \InvalidArgumentException(sprintf('Invalid email "%s".', $email)); + } + + if ($isBcc) { + if (!in_array($email, $this->bccRecipients)) { + $this->bccRecipients[] = $email; + } + } else { + if (!in_array($email, $this->recipients)) { + $this->recipients[] = $email; + } + } + + return $this; + } + + /** + * @return EmailNotifier + */ + public function addRecipients(array $emails, bool $isBcc = false): self + { + foreach ($emails as $email) { + $this->addRecipient($email, $isBcc); + } + + return $this; + } + + /** + * @return EmailNotifier + */ + public function addRecipientByAccount(Account $account, bool $isBcc = false): self + { + return $this->addRecipient($account->getEmail(), $isBcc); + } + + /** + * @param mixed $accounts + * + * @return EmailNotifier + */ + public function addRecipientsByAccounts($accounts, bool $isBcc = false) + { + if (!is_array($accounts)) { + throw new InvalidArgumentException('The "accounts" parameter must be an array or an instance of ObjectCollection'); + } + + foreach ($accounts as $account) { + $this->addRecipientByAccount($account, $isBcc); + } + + return $this; + } + + /** + * @return EmailNotifier + */ + public function addAttachment(string $attachment): self + { + if (!in_array($attachment, $this->attachments)) { + $this->attachments[] = $attachment; + } + + return $this; + } + + /** + * @return EmailNotifier + */ + public function addAttachments(array $attachments): self + { + foreach ($attachments as $attachment) { + $this->addAttachment($attachment); + } + + return $this; + } + + /** + * @return EmailNotifier + */ + public function init(): self + { + $this + ->setSubject(null) + ->setRecipients([]) + ->setBccRecipients([]) + ->setAttachments([]) + ; + + return $this; + } + + /** + * @return EmailNotifier + */ + public function notify(string $template, array $data = [], string $type = 'text/html'): self + { + $message = $this->createMessage( + $this->twig->render( + sprintf('mail/%s.html.twig', $template), + $data + ), + $type + ); + + $this->mailer->send($message); + + return $this; + } + + protected function createMessage(string $body, string $type = 'text/html'): Swift_Message + { + $message = new Swift_Message(); + + if ($this->getSubject()) { + $message->setSubject($this->getSubject()); + } + + if ($this->getFrom()) { + $message->setFrom($this->getFrom()); + } + + if ($this->getReplyTo()) { + $message->setReplyTo($this->getReplyTo()); + } + + if (count($this->getRecipients()) > 0) { + $message->setTo($this->getRecipients()); + } + + if (count($this->getBccRecipients()) > 0) { + $message->setBcc($this->getBccRecipients()); + } + + foreach ($this->getAttachments() as $attachment) { + if (is_object($attachment) && $attachment instanceof Swift_Attachment) { + $message->attach($attachment); + } elseif (is_string($attachment) && file_exists($attachment) && is_readable($attachment) && !is_dir($attachment)) { + $message->attach(Swift_Attachment::fromPath($attachment)); + } + } + + $message->setBody($body, $type); + + return $message; + } +} diff --git a/core/Repository/.gitignore b/core/Repository/.gitignore new file mode 100644 index 0000000..e69de29 diff --git a/core/Repository/RepositoryQuery.php b/core/Repository/RepositoryQuery.php new file mode 100644 index 0000000..27addf9 --- /dev/null +++ b/core/Repository/RepositoryQuery.php @@ -0,0 +1,96 @@ + + */ +abstract class RepositoryQuery +{ + protected ServiceEntityRepository $repository; + protected QueryBuilder $query; + protected PaginatorInterface $paginator; + protected string $id; + + public function __construct(ServiceEntityRepository $repository, string $id, PaginatorInterface $paginator = null) + { + $this->repository = $repository; + $this->query = $repository->createQueryBuilder($id); + $this->paginator = $paginator; + $this->id = $id; + } + + public function __call(string $name, $params): self + { + $fn = function (&$data) { + if (is_string($data)) { + $words = explode(' ', $data); + + foreach ($words as $k => $v) { + if (isset($v[0]) && '.' === $v[0]) { + $words[$k] = $this->id.$v; + } + } + + $data = implode(' ', $words); + } elseif (is_array($data)) { + foreach ($data as $k => $v) { + $fn($data[$k]); + } + } + + return $data; + }; + + foreach ($params as $key => $value) { + $fn($params[$key]); + } + + call_user_func_array([$this->query, $name], $params); + + return $this; + } + + public function create() + { + $class = get_called_class(); + + return new $class($this->repository, $this->paginator); + } + + public function call(callable $fn): self + { + $fn($this->query, $this); + + return $this; + } + + public function findOne() + { + return $this->query->getQuery() + ->setMaxResults(1) + ->getOneOrNullResult() + ; + } + + public function find() + { + return $this->query->getQuery()->getResult(); + } + + public function paginate(int $page = 1, int $limit = 20) + { + return $this->paginator->paginate($this->query->getQuery(), $page, $limit); + } + + public function getRepository(): ServiceEntityRepository + { + return $this->repository; + } +} diff --git a/core/Repository/Site/MenuRepository.php b/core/Repository/Site/MenuRepository.php new file mode 100644 index 0000000..6e7b591 --- /dev/null +++ b/core/Repository/Site/MenuRepository.php @@ -0,0 +1,15 @@ + + */ +class MenuRepositoryQuery extends RepositoryQuery +{ + public function __construct(MenuRepository $repository, PaginatorInterface $paginator) + { + parent::__construct($repository, 'm', $paginator); + } +} diff --git a/core/Repository/Site/NavigationRepository.php b/core/Repository/Site/NavigationRepository.php new file mode 100644 index 0000000..987b35e --- /dev/null +++ b/core/Repository/Site/NavigationRepository.php @@ -0,0 +1,15 @@ + + */ +class NavigationRepositoryQuery extends RepositoryQuery +{ + public function __construct(NavigationRepository $repository, PaginatorInterface $paginator) + { + parent::__construct($repository, 'n', $paginator); + } +} diff --git a/core/Repository/Site/NodeRepository.php b/core/Repository/Site/NodeRepository.php new file mode 100644 index 0000000..c83e32e --- /dev/null +++ b/core/Repository/Site/NodeRepository.php @@ -0,0 +1,38 @@ +getClassMetadata(Node::class)); + } + + public function urlExists($url, Node $node) + { + $query = $this->createQueryBuilder('n') + ->join('n.menu', 'm') + ->where('n.url = :url') + ->andWhere('m.navigation = :navigation') + ->setParameter(':url', $url) + ->setParameter(':navigation', $node->getMenu()->getNavigation()) + ; + + if ($node->getId()) { + $query + ->andWhere('n.id != :id') + ->setParameter(':id', $node->getId()) + ; + } + + return $query->getQuery() + ->setMaxResults(1) + ->getOneOrNullResult() + ; + } +} diff --git a/core/Repository/Site/Page/BlockRepository.php b/core/Repository/Site/Page/BlockRepository.php new file mode 100644 index 0000000..1ccca24 --- /dev/null +++ b/core/Repository/Site/Page/BlockRepository.php @@ -0,0 +1,15 @@ + + */ +class BlockRepositoryQuery extends RepositoryQuery +{ + public function __construct(BlockRepository $repository, PaginatorInterface $paginator) + { + parent::__construct($repository, 'b', $paginator); + } +} diff --git a/core/Repository/Site/Page/PageRepository.php b/core/Repository/Site/Page/PageRepository.php new file mode 100644 index 0000000..9338c67 --- /dev/null +++ b/core/Repository/Site/Page/PageRepository.php @@ -0,0 +1,15 @@ + + */ +class PageRepositoryQuery extends RepositoryQuery +{ + public function __construct(PageRepository $repository, PaginatorInterface $paginator) + { + parent::__construct($repository, 'p', $paginator); + } + + public function filterById($id) + { + $this + ->where('.id = :id') + ->setParameter(':id', $id) + ; + + return $this; + } +} diff --git a/core/Router/SiteRouteLoader.php b/core/Router/SiteRouteLoader.php new file mode 100644 index 0000000..8bd535c --- /dev/null +++ b/core/Router/SiteRouteLoader.php @@ -0,0 +1,69 @@ + + */ +class SiteRouteLoader extends Loader +{ + protected NavigationRepositoryQuery $navigationQuery; + protected $isLoaded = false; + + public function __construct(NavigationRepositoryQuery $navigationQuery) + { + $this->navigationQuery = $navigationQuery; + } + + public function load($resource, ?string $type = null) + { + if (true === $this->isLoaded) { + throw new \RuntimeException('Do not add the "extra" loader twice'); + } + + $routes = new RouteCollection(); + $navigations = $this->navigationQuery->find(); + + foreach ($navigations as $navigation) { + foreach ($navigation->getMenus() as $menu) { + foreach ($menu->getRootNode()->getAllChildren() as $node) { + if ($node->getParent() === null) { + continue; + } + + $requirements = []; + + $defaults = [ + '_controller' => PageController::class.'::show', + '_node' => $node->getId(), + '_menu' => $menu->getId(), + '_page' => $node->getPage() ? $node->getPage()->getId() : null, + '_navigation' => $navigation->getId(), + ]; + + $route = new Route($node->getUrl(), $defaults, $requirements); + $route->setHost($navigation->getDomain()); + + $routes->add($node->getRouteName(), $route); + } + } + } + + $this->isLoaded = true; + + return $routes; + } + + public function supports($resource, string $type = null) + { + return 'extra' === $type; + } +} diff --git a/core/Security/TokenGenerator.php b/core/Security/TokenGenerator.php new file mode 100644 index 0000000..bb8b218 --- /dev/null +++ b/core/Security/TokenGenerator.php @@ -0,0 +1,13 @@ + + */ +class PageConfiguration +{ + protected string $className; + protected string $name; + protected array $templates; + + public function setClassName(string $className): self + { + $this->className = $className; + + return $this; + } + + public function getClassName(): string + { + return $this->className; + } + + public function setName(string $name): self + { + $this->name = $name; + + return $this; + } + + public function getName(): string + { + return $this->name; + } + + public function setTemplates(array $templates): self + { + $this->templates = $templates; + + return $this; + } + + public function getTemplates(): array + { + return $this->templates; + } +} diff --git a/core/Site/PageLocator.php b/core/Site/PageLocator.php new file mode 100644 index 0000000..b9cea42 --- /dev/null +++ b/core/Site/PageLocator.php @@ -0,0 +1,48 @@ + + */ +class PageLocator +{ + protected array $params; + protected array $pages; + + public function __construct(ParameterBagInterface $bag) + { + $this->params = $bag->get('app'); + $this->loadPages(); + } + + public function getPages(): array + { + return $this->pages; + } + + public function getPage($className) + { + return $this->pages[$className] ?? null; + } + + protected function loadPages(): void + { + $params = $this->params['site']['pages'] ?? []; + + foreach ($params as $className => $conf) { + $pageConfiguration = new PageConfiguration(); + $pageConfiguration + ->setClassName($className) + ->setName($conf['name']) + ->setTemplates($conf['templates']) + ; + + $this->pages[$className] = $pageConfiguration; + } + } +} diff --git a/core/Site/SiteRequest.php b/core/Site/SiteRequest.php new file mode 100644 index 0000000..55c9f20 --- /dev/null +++ b/core/Site/SiteRequest.php @@ -0,0 +1,69 @@ + + */ +class SiteRequest +{ + protected RequestStack $requestStack; + protected NodeRepository $nodeRepository; + protected NavigationRepositoryQuery $navigationRepositoryQuery; + protected PageRepositoryQuery $pageRepositoryQuery; + + public function __construct(RequestStack $requestStack, NodeRepository $nodeRepository) + { + $this->requestStack = $requestStack; + $this->nodeRepository = $nodeRepository; + } + + public function getNode(): ?Node + { + $request = $this->requestStack->getCurrentRequest(); + + if ($request->attributes->has('_node')) { + return $this->nodeRepository->findOneBy([ + 'id' => $request->attributes->get('_node'), + ]); + } + + return null; + } + + public function getPage(): ?Page + { + $node = $this->getNode(); + + if ($node && $node->getPage()) { + return $node->getPage(); + } + + return null; + } + + public function getMenu(): ?Menu + { + $node = $this->getNode(); + + return null !== $node ? $node->getMenu() : null; + } + + public function getNavigation(): ?Navigation + { + $menu = $this->getMenu(); + + return null !== $menu ? $menu->getNavigation() : null; + } +} diff --git a/core/Slugify/CodeSlugify.php b/core/Slugify/CodeSlugify.php new file mode 100644 index 0000000..13236a7 --- /dev/null +++ b/core/Slugify/CodeSlugify.php @@ -0,0 +1,26 @@ + + */ +class CodeSlugify extends Slugify +{ + protected function create(): BaseSlugify + { + $slugify = new BaseSlugify([ + 'separator' => '_', + 'lowercase' => true, + ]); + + $slugify->activateRuleSet('french'); + $slugify->addRule("'", ''); + + return $slugify; + } +} diff --git a/core/Slugify/Slugify.php b/core/Slugify/Slugify.php new file mode 100644 index 0000000..965a3ae --- /dev/null +++ b/core/Slugify/Slugify.php @@ -0,0 +1,31 @@ + + */ +class Slugify +{ + public function slugify($data) + { + return $this->create()->slugify($data); + } + + protected function create(): BaseSlugify + { + $slugify = new BaseSlugify([ + 'separator' => '-', + 'lowercase' => true, + ]); + + $slugify->activateRuleSet('french'); + $slugify->addRule("'", ''); + + return $slugify; + } +}