From f27cca08ff786ceff1870147f6d6bad14d71117f Mon Sep 17 00:00:00 2001
From: polo , téléchargement côté serveur et renvoi de l'adresse sur le serveur
+ static public function uploadImageHtml(): JsonResponse
+ {
+ $json = json_decode(file_get_contents('php://input'), true);
+
+ if(!isset($json['image_url'])){
+ return new JsonResponse(['message' => "Erreur 400: Bad Request"], JsonResponse::HTTP_BAD_REQUEST); // code 400
+ }
+
+ $image_data = self::curlDownloadImage($json['image_url']); // téléchargement de l’image par le serveur avec cURL au lieu de file_get_contents
+ if(!$image_data){
+ return new JsonResponse(['message' => "Erreur, le serveur n'a pas réussi à télécharger l'image."], JsonResponse::HTTP_BAD_REQUEST); // code 400
+ }
+
+ $dest = 'user_data/images/';
+ if(!is_dir($dest)){ // Vérifier si le répertoire existe, sinon le créer
+ mkdir($dest, 0755, true);
+ }
+
+ $url_path = parse_url($json['image_url'], PHP_URL_PATH);
+ $name = Security::secureFileName(pathinfo($url_path, PATHINFO_FILENAME));
+ $extension = strtolower(pathinfo($url_path, PATHINFO_EXTENSION));
+ if(!in_array($extension, self::ALLOWED_EXTENSIONS) || $extension === 'jpg'){
+ $extension = 'jpeg';
+ }
+ $local_path = uniqid($dest . $name . '_') . '.' . $extension;
+
+ if(self::imagickCleanAndWriteImage($image_data, $local_path)){ // recréer l’image pour la nettoyer
+ return new JsonResponse(['location' => $local_path]); // nouvelle adresse
+ }
+ else{
+ return new JsonResponse(['message' => 'Erreur image non valide', 'format' => $extension], JsonResponse::HTTP_INTERNAL_SERVER_ERROR); // code 500
+ }
+ }
+
+ // collage simple d'une image (base64 dans le presse-papier) non encapsulée dans du HTML
+ static public function uploadImageBase64(): JsonResponse
+ {
+ $json = json_decode(file_get_contents('php://input'), true);
+ $dest = 'user_data/images/';
+
+ if(!is_dir($dest)){
+ mkdir($dest, 0755, true);
+ }
+
+ // détection de data:image/ et de ;base64, et capture du format dans $type
+ if(!isset($json['image_base64']) || !preg_match('/^data:image\/(\w+);base64,/', $json['image_base64'], $type)){
+ return new JsonResponse(['message' => 'Données image base64 manquantes ou invalides'], JsonResponse::HTTP_BAD_REQUEST); // code 400
+ }
+
+ $extension = strtolower($type[1]); // dans (\w+)
+ if(!in_array($extension, self::ALLOWED_EXTENSIONS)){
+ $extension = 'jpeg';
+ }
+
+ $name = 'pasted_image';
+ $image_data = base64_decode(substr($json['image_base64'], strpos($json['image_base64'], ',') + 1)); // découpe la chaine à la virgule puis convertit en binaire
+ if($image_data === false){
+ return new JsonResponse(['message' => 'Décodage base64 invalide'], JsonResponse::HTTP_BAD_REQUEST); // code 400
+ }
+ $local_path = uniqid($dest . $name . '_') . '.' . $extension;
+
+ if(self::imagickCleanAndWriteImage($image_data, $local_path)){
+ return new JsonResponse(['location' => $local_path]);
+ }
+ else{
+ return new JsonResponse(['message' => 'Erreur image non valide', 'format' => $extension], JsonResponse::HTTP_INTERNAL_SERVER_ERROR); // code 500
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Controller/MaintenanceController.php b/src/Controller/MaintenanceController.php
new file mode 100644
index 0000000..d3be2a1
--- /dev/null
+++ b/src/Controller/MaintenanceController.php
@@ -0,0 +1,152 @@
+getRepository(Log::class)->findAll();
+ if(empty($data)){
+ return new JsonResponse(['success' => false]);
+ }
+ else{
+ $view = '
Table ' . TABLE_PREFIX . 'log de la base de données
+
+
+
';
+ return new JsonResponse(['success' => true, 'view' => $view]);
+ }
+ }
+ static public function eraseLogs(EntityManager $entityManager): JsonResponse
+ {
+ try{
+ $table = $entityManager->getClassMetadata(Log::class)->getTableName();
+ $entityManager->getConnection()->executeStatement("TRUNCATE TABLE {$table}"); // SQL donné à DBAL
+ return new JsonResponse(['success' => true]);
+ }
+ catch(\Exception $e){
+ return new JsonResponse(['success' => false, 'error' => $e->getMessage()]);
+ }
+ }
+
+ static public function getLastDump(EntityManager $entityManager): BinaryFileResponse|RedirectResponse
+ {
+ try{
+ $backup_list = Backup::getBackupList();
+ $nb = count($backup_list);
+
+ if($nb <= 0){ // se produit à la première connexion en mode admin pour une raison algorithimque
+ Backup::mySQLdump($entityManager, 'auto');
+ $backup_list = Backup::getBackupList();
+ $nb = count($backup_list);
+ if($nb <= 0){ // improbable, les dossiers devraient déjà avoir été créés
+ throw new \RuntimeException("Le serveur a rencontré une erreur: aucun backup n'est disponible et ce n'est pas normal.");
+ }
+ }
+
+ $file_path = Backup::$backup_dir . '/' . $backup_list[$nb - 1];
+ $response = new BinaryFileResponse($file_path);
+ $response->setContentDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT); // ne pas essayer de l'afficher dans le navigateur
+ }
+ catch(\RuntimeException $e){
+ $_SESSION['flash_message'] = $e->getMessage();
+ $response = new RedirectResponse((string) new URL(['page' => 'maintenance']));
+ }
+ return $response;
+ }
+ static public function getAllMedia(): BinaryFileResponse|RedirectResponse
+ {
+ try{
+ $file_path = '../var/' . Backup::createZip('all_media.zip', ['user_data/assets', 'user_data/images', 'user_data/media']);
+ $response = new BinaryFileResponse($file_path);
+ $response->setContentDisposition(ResponseHeaderBag::DISPOSITION_ATTACHMENT); // ne pas essayer de l'afficher dans le navigateur
+ }
+ catch(\RuntimeException $e){
+ $_SESSION['flash_message'] = $e->getMessage();
+ $response = new RedirectResponse((string) new URL(['page' => 'maintenance']));
+ }
+ return $response;
+ }
+
+ // parce qu'il faut un contrôleur
+ static public function handleBackupSelection(EntityManager $entityManager, Request $request): RedirectResponse
+ {
+ $selected_file = $request->request->get('selected_sql');
+ $url = new URL;
+ if($request->query->has('from')){
+ $url->addParams(['page' => $request->query->get('from')]);
+ }
+
+ try{
+ if(pathinfo($selected_file)['extension'] !== 'sql'){ // pas censé se produire en fait
+ throw new \Exception("charger un fichier au format SQL");
+ }
+ Backup::restoreDatabase($entityManager, $selected_file, ['user']);
+
+ $_SESSION['flash_message'] = "La base de données a été restaurée avec succès !!";
+ }
+ catch(\Exception $e){
+ $_SESSION['flash_message'] = "Une erreur s'est produite: " . $e->getMessage();
+ }
+
+ return new RedirectResponse((string)$url);
+ }
+
+ static public function downloadSQL(EntityManager $entityManager, Request $request): RedirectResponse
+ {
+ $uploaded_file = $request->files->get('uploaded_sql');
+ $date = new \DateTime;
+ $server_place = Config::$database . '_' . $date->format('Y-m-d') . '_uploaded.sql';
+ $url = new URL;
+ if($request->query->has('from')){
+ $url->addParams(['page' => $request->query->get('from')]);
+ }
+
+ try{
+ if(pathinfo($uploaded_file->getClientOriginalName())['extension'] !== 'sql'){
+ throw new \Exception("Charger un fichier au format SQL");
+ }
+ //echo $uploaded_file->getSize(); // à garder de côté au cas où
+
+ // enregistrer le fichier
+ $uploaded_file->move(Backup::$backup_dir, $server_place);
+
+ // s'en servir
+ Backup::restoreDatabase($entityManager, $server_place, ['user']);
+
+ $_SESSION['flash_message'] = "La base de données a été restaurée avec succès !!";
+ }
+ catch(\Exception $e){
+ $_SESSION['flash_message'] = "Une erreur s'est produite: " . $e->getMessage();
+ }
+
+ return new RedirectResponse((string)$url);
+ }
+}
\ No newline at end of file
diff --git a/src/Controller/MenuAndPathsController.php b/src/Controller/MenuAndPathsController.php
new file mode 100644
index 0000000..1916f06
--- /dev/null
+++ b/src/Controller/MenuAndPathsController.php
@@ -0,0 +1,226 @@
+findPageById((int)$_POST["location"]); // (int) à cause de declare(strict_types=1);
+ $parent = $previous_page->getParent();
+
+ $url_input = trim($_POST["url_input"]); // faire htmlspecialchars à l'affichage
+ if(!filter_var($url_input, FILTER_VALIDATE_URL) || !str_starts_with($url_input, 'http')){
+ // utiliser une flash error
+ return new RedirectResponse((string)new URL(['page' => $_GET['from'], 'error' => 'invalide_url']));
+ }
+
+ $page = new Page(
+ trim(htmlspecialchars($_POST["label_input"])),
+ $url_input, '',
+ true, true, false,
+ $previous_page->getPosition(),
+ $parent); // peut et DOIT être null si on est au 1er niveau
+
+ // on a donné à la nouvelle entrée la même position qu'à la précédente,
+ // addChild l'ajoute à la fin du tableau "children" puis on trie
+ // exemple avec 2 comme position demandée: 1 2 3 4 2 devient 1 2 3 4 5 et la nouvelle entrée sera en 3è position
+ if(!$parent){
+ $parent = Model::$menu;
+ }
+ $parent->addChild($page); // true pour réindexer les positions en BDD
+ $parent->reindexPositions();
+
+ $entityManager->persist($page);
+ $entityManager->flush();
+ return new RedirectResponse((string)new URL(['page' => $_GET['from']]));
+ }
+
+ // on pourrait utiliser FormValidation ici
+ static public function editUrl(EntityManager $entityManager, array $json): JsonResponse
+ {
+ $url_data = trim($json['input_data']); // garder htmlspecialchars pour l'affichage
+ $page = $entityManager->find('App\Entity\Page', $json['id']);
+
+ if(!$page){
+ return new JsonResponse(['success' => false, 'message' => "id invalide"]);
+ }
+ elseif(!in_array($json['field'], ['url_name', 'url_content'])){
+ return new JsonResponse(['success' => false, 'message' => "champ invalide"]);
+ }
+ elseif($json['field'] === 'url_content' && (!filter_var($url_data, FILTER_VALIDATE_URL) || !str_starts_with($url_data, 'http'))){
+ return new JsonResponse(['success' => false, 'message' => "la chaîne envoyée n'est pas une URL valide"]);
+ }
+ else{
+ if($json['field'] === 'url_name'){
+ $page->setPageName($url_data);
+ }
+ elseif($json['field'] === 'url_content'){
+ $page->setEndOfPath($url_data);
+ }
+ $entityManager->flush();
+ return new JsonResponse(['success' => true, 'url_data' => $url_data]);
+ }
+ }
+
+ static public function deleteUrlMenuEntry(EntityManager $entityManager): RedirectResponse
+ {
+ Model::$menu = new Menu($entityManager);
+ $page = Model::$menu->findPageById((int)$_POST["delete"]);
+ $parent = $page->getParent();
+ if($parent == null){
+ $parent = Model::$menu;
+ }
+
+ $parent->removeChild($page); // suppression de $children avant de trier
+ $parent->reindexPositions();
+
+ $entityManager->remove($page); // suppression en BDD
+ $entityManager->flush();
+ return new RedirectResponse((string)new URL(['page' => $_GET['from']]));
+ }
+
+ static public function MoveOneLevelUp(EntityManager $entityManager, array $json): JsonResponse
+ {
+ $id = $json['id'];
+ $page = Model::$menu->findPageById((int)$id);
+
+ $parent = $page->getParent(); // peut être null
+ if($parent === null){
+ // 1er niveau: ne rien faire
+ return new JsonResponse(['success' => false, 'message' => 'nothing to do']);
+ }
+ // BDD
+ else{
+ $page->setPosition($parent->getPosition() + 1); // nouvelle position
+
+ // 2ème niveau: le parent devient $menu, puis null après tri
+ if($parent->getParent() === null){
+ // connexion dans les deux sens
+ $page->setParent(Model::$menu); // => pour la persistance
+
+ //Model::$menu->addChild($page); // => pour sortChildren
+ $page->getParent()->addChild($page); // => pour sortChildren
+ $page->getParent()->sortChildren(true); // positions décaléees des nouveaux petits frères
+ $page->setParent(null);
+
+ // affichage
+ $page->setPagePath($page->getEndOfPath());
+ $page->fillChildrenPagePath();
+ }
+ // 3ème niveau et plus
+ else{
+ $page->setParent($parent->getParent()); // nouveau parent
+ $page->getParent()->addChild($page); // => pour sortChildren
+ $page->getParent()->sortChildren(true); // positions décaléees des nouveaux petits frères
+ $page->fillChildrenPagePath($page->getParent()->getPagePath());
+ }
+ $entityManager->flush();
+
+ // affichage
+ $parent->removeChild($page);
+ $nav_builder = new NavBuilder();
+ $menu_builder = new MenuBuilder(null, false);
+ return new JsonResponse(['success' => true, 'nav' => $nav_builder->render(), 'menu_buttons' => $menu_builder->render()]);
+ }
+ }
+
+ static public function MoveOneLevelDown(EntityManager $entityManager, array $json): JsonResponse
+ {
+ $id = $json['id'];
+ $page = Model::$menu->findPageById((int)$id);
+
+ $parent = $page->getParent(); // peut être null
+ if($parent === null){
+ $parent = Model::$menu;
+ }
+
+ // BDD
+ $parent->sortChildren(true); // trie et réindexe par sécurité: 1, 2, 3...
+ if($page->getPosition() > 1){
+ foreach($parent->getChildren() as $child){
+ if($child->getPosition() === $page->getPosition() - 1){
+ // refus si $parent est une adresse, ça va casser le lien, exemple: index.php?page=chemin/http://un_site_web.fr/vers/ici
+ if(str_starts_with($child->getEndOfPath(), 'http')){
+ return new JsonResponse(['success' => false, 'error' => 'new_parent_is_a_link']);
+ }
+
+ $page->setParent($child);
+ break;
+ }
+ }
+ $page->setPosition(count($page->getParent()->getChildren()) + 1);
+ }
+ $entityManager->flush();
+
+ // affichage
+ $parent->removeChild($page);
+ $page->getParent()->addChild($page);
+ $page->fillChildrenPagePath($page->getParent()->getPagePath()); // variable non mappée $page_path
+ $nav_builder = new NavBuilder();
+ $menu_builder = new MenuBuilder(null, false);
+
+ return new JsonResponse(['success' => true, 'nav' => $nav_builder->render(), 'menu_buttons' => $menu_builder->render()]);
+ }
+
+ static public function SwitchPositions(EntityManager $entityManager, array $json): JsonResponse
+ {
+ $id1 = $json['id1'];
+ $id2 = $json['id2'];
+
+ // vérifier qu'ils ont le même parent
+ $page1 = Model::$menu->findPageById((int)$id1);
+ $page2 = Model::$menu->findPageById((int)$id2);
+
+ // double le contrôle fait en JS
+ if($page1->getParent() === $page2->getParent()) // comparaison stricte d'objet (même instance du parent?)
+ {
+ // inversion
+ $tmp = $page1->getPosition();
+ $page1->setPosition($page2->getPosition());
+ $page2->setPosition($tmp);
+ Model::$menu->sortChildren(true); // modifie tableau children
+ $entityManager->flush();
+
+ // nouveau menu
+ $nav_builder = new NavBuilder();
+ return new JsonResponse(['success' => true, 'nav' => $nav_builder->render()]);
+ }
+ else{
+ return new JsonResponse(['success' => false]);
+ }
+ }
+
+ static public function displayInMenu(EntityManager $entityManager, array $json): JsonResponse
+ {
+ $id = $json['id'];
+ $checked = $json['checked'];
+
+ $page = Model::$menu->findPageById((int)$id);
+ if($page->isHidden() === $checked){
+ $page->setHidden(!$checked);
+ $entityManager->flush();
+
+ // nouveau menu
+ $nav_builder = new NavBuilder();
+ return new JsonResponse(['success' => true, 'nav' => $nav_builder->render()]);
+ }
+ else{
+ return new JsonResponse(['success' => false]);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Controller/PageManagementController.php b/src/Controller/PageManagementController.php
new file mode 100644
index 0000000..02e6e6a
--- /dev/null
+++ b/src/Controller/PageManagementController.php
@@ -0,0 +1,368 @@
+find('App\Entity\Page', $json['page_id']);
+ $page->setPageName(htmlspecialchars($json['title']));
+ $entityManager->flush();
+ return new JsonResponse(['success' => true, 'title' => $page->getPageName()]);
+ }
+
+ static public function updatePageMenuPath(EntityManager $entityManager, string $page_menu_path): RedirectResponse
+ {
+ Model::$menu = new Menu($entityManager);
+ Model::$page_path = new Path();
+ $page = Model::$page_path->getLast();
+ $path = htmlspecialchars($page_menu_path);
+
+ // mise en snake_case: filtre caractères non-alphanumériques, minuscule, doublons d'underscore, trim des underscores
+ $path = trim(preg_replace('/_+/', '_', strtolower(preg_replace('/[^a-zA-Z0-9]/', '_', $path))), '_');
+ $page->setEndOfPath($path);
+ foreach(Model::$menu->getChildren() as $child){
+ if($child->getEndOfPath() === Model::$page_path->getArray()[0]->getEndOfPath()){
+ $child->fillChildrenPagePath(); // MAJ de $page_path
+ }
+ }
+ $entityManager->flush();
+ return new RedirectResponse((string)new URL(['page' => $page->getPagePath(), 'mode' => 'page_modif']));
+ }
+
+ static public function setPageDescription(EntityManager $entityManager, array $json): JsonResponse
+ {
+ $page = $entityManager->find('App\Entity\Page', $json['page_id']);
+ $page->setDescription(htmlspecialchars($json['description']));
+ $entityManager->flush();
+ return new JsonResponse(['success' => true, 'description' => $page->getDescription()]);
+ }
+
+ static public function newPage(EntityManager $entityManager, InputBag $post): RedirectResponse
+ {
+ // titre et chemin
+ Model::$menu = new Menu($entityManager);
+ $previous_page = Model::$menu->findPageById((int)$post->get("page_location")); // (int) à cause de declare(strict_types=1);
+ $parent = $previous_page->getParent();
+
+ $page = new Page(
+ trim(htmlspecialchars($post->get("page_name"))),
+ trim(htmlspecialchars($post->get("page_name_path"))),
+ trim(htmlspecialchars($post->get("page_description"))),
+ true, true, false,
+ $previous_page->getPosition(),
+ $parent); // peut et DOIT être null si on est au 1er niveau
+
+ // on a donné à la nouvelle entrée la même position qu'à la précédente,
+ // addChild l'ajoute à la fin du tableau "children" puis on trie
+ // exemple avec 2 comme position demandée: 1 2 3 4 2 devient 1 2 3 4 5 et la nouvelle entrée sera en 3è position
+ if($parent == null){
+ $parent = Model::$menu;
+ }
+ $parent->addChild($page);
+ $parent->reindexPositions();
+
+ $page->setPagePath(ltrim($parent->getPagePath() . '/' . $page->getEndOfPath(), '/'));
+
+ $entityManager->persist($page);
+ $entityManager->flush();
+
+ // page créée, direction la page en mode modification pour ajouter des blocs
+ return new RedirectResponse((string)new URL(['page' => $page->getPagePath(), 'mode' => 'page_modif']));
+ }
+
+ static public function deletePage(EntityManager $entityManager, string $page_id): RedirectResponse
+ {
+ $menu = new Menu($entityManager);
+ $page = $menu->findPageById((int)$page_id);
+ $url = new URL;
+
+ // test dernière page
+ // => $menu a un enfant et pas de petits enfants
+ // => $menu->getChildren()->isEmpty() n'est théoriquement pas possible
+ if($menu->getChildren()->isEmpty() || (count($menu->getChildren()) === 1 && $menu->getChildren()[0]->getChildren()->isEmpty())){
+ $url->addParams(['page' => $page->getEndOfPath(), 'error' => 'you_tried_to_remove_the_last_page']);
+ }
+ else{
+ $nodes = $entityManager->getRepository('App\Entity\Node')->findBy(['page' => $page]);
+ $data = [];
+ foreach($nodes as $node){
+ $data[] = $entityManager->getRepository('App\Entity\NodeData')->findOneBy(['node' => $node]);
+ $entityManager->remove($node);
+ }
+ foreach($data as $one_data){
+ $entityManager->remove($one_data);
+ }
+ $entityManager->remove($page); // suppression en BDD
+ $entityManager->flush();
+
+ // destination vers la première page dispo
+ // traite les cas prévus dans le if plus haut
+ $menu->removeChild($page);
+ $next_page = $menu->getChildren()->isEmpty() ? $next_page = $page->getChildren()[0] : $menu->getChildren()[0];
+ $url->addParams(['page' => $next_page->getEndOfPath()]);
+ }
+
+ return new RedirectResponse((string)$url);
+ }
+
+ /* partie "blocs" */
+ static public function addBloc(EntityManager $entityManager, Request $request): RedirectResponse
+ {
+ $model = new Model($entityManager);
+ $model->makeMenuAndPaths(); // on a besoin de page_path qui dépend de menu
+ $page = Model::$page_path->getLast();
+ $model->findUniqueNodeByName('main');
+ $model->findItsChildren();
+ $main = $model->getNode();
+ $position = count($main->getChildren()) + 1; // position dans la fraterie
+
+ if(!in_array($request->request->get("bloc_select"), array_keys(Presentation::BLOCKS), true)){ // 3è param: contrôle du type
+ // utiliser une flash error
+ return new RedirectResponse((string)new URL(['page' => $request->query->get('page'), 'error' => 'bad_bloc_type']));
+ }
+
+ if(in_array($request->request->get("bloc_select"), ['calendar', 'form'])){
+ $page->addCSS($request->request->get("bloc_select"));
+ if($request->request->get("bloc_select") === 'form'){
+ $page->addJS($request->request->get("bloc_select"));
+ }
+ $entityManager->persist($page);
+ }
+
+ $block = new Node($request->request->get("bloc_select"), $position, $main, $page);
+
+ $DataClass = match($request->request->get("bloc_select")){
+ 'form' => EmailForm::class,
+ 'post_block', 'news_block' => Presentation::class,
+ 'calendar' => NodeData::class,
+ default => throw new \InvalidArgumentException("Erreur: type de bloc inconnu"),
+ };
+ $data = new $DataClass(['title' => trim(htmlspecialchars($request->request->get("bloc_title")))], $block);
+
+ // valeurs par défaut
+ if($request->request->get("bloc_select") === 'post_block'){
+ $data->setPresentation('fullwidth');
+ }
+ elseif($request->request->get("bloc_select") === 'news_block'){
+ $data->setPresentation('grid');
+ }
+ elseif($request->request->get("bloc_select") === 'galery'){
+ $data->setPresentation('mosaic'); // un jour on mettra carousel
+ }
+ // else = null par défaut
+
+ $entityManager->persist($block);
+ $entityManager->persist($data);
+ $entityManager->flush();
+ return new RedirectResponse((string)new URL(['page' => $request->query->get('page'), 'mode' => 'page_modif']));
+ }
+
+ static public function deleteBloc(EntityManager $entityManager, Request $request): RedirectResponse
+ {
+ $model = new Model($entityManager);
+ $model->makeMenuAndPaths();
+ $model->findUniqueNodeByName('main');
+ $model->findItsChildren();
+ $main = $model->getNode();
+
+ $block = null;
+ $type = '';
+ $nb_same_type = 0;
+ foreach($main->getChildren() as $child){
+ if($child->getId() === (int)$request->request->get('delete_bloc_id')){
+ $block = $child;
+ $type = $block->getName();
+ }
+ if($child->getName() === $type){
+ $nb_same_type++;
+ }
+ }
+
+ // nettoyage fichiers CSS et JS si on retire le derner bloc de ce type
+ if($nb_same_type === 1 && in_array($block->getName(), ['calendar', 'form'])){
+ $page = $block->getPage();
+ $page->removeCSS($block->getName());
+ if($block->getName() === 'form'){
+ $page->removeJS($block->getName());
+ }
+ }
+
+ if(!empty($block)){ // si $block est null c'est que le HTML a été modifié volontairement
+ $main->removeChild($block); // réindex le tableau $children au passage
+ $main->reindexPositions();
+ if(isset($page)){
+ $entityManager->persist($page);
+ }
+ $block->getNodeData()->setNode(null);
+ $entityManager->remove($block);
+ try{
+ $entityManager->flush();
+ }
+ catch(\Exception $e){
+ // utiliser une flash error
+ return new RedirectResponse((string)new URL(['page' => $request->query->get('page'), 'mode' => 'page_modif', 'error' => $e->getMessage()]));
+ }
+ }
+
+ return new RedirectResponse((string)new URL(['page' => $request->query->get('page'), 'mode' => 'page_modif']));
+ }
+
+ static public function renameBloc(EntityManager $entityManager, array $json): JsonResponse
+ {
+ if(isset($json['bloc_title']) && $json['bloc_title'] !== null && isset($json['bloc_id']) && is_int($json['bloc_id'])){
+ $model = new Model($entityManager);
+ $model->findNodeById($json['bloc_id']);
+
+ // le titre (du JSON en BDD) est récupéré sous forme de tableau, modifié et renvoyé
+ $data = $model->getNode()->getNodeData()->getData();
+ $data['title'] = htmlspecialchars($json['bloc_title']);
+ $model->getNode()->getNodeData()->updateData('title', htmlspecialchars($json['bloc_title']));
+
+ $entityManager->flush();
+ return new JsonResponse(['success' => true, 'title' => $data['title']]);
+ }
+ else{
+ return new JsonResponse(['success' => false]);
+ }
+ }
+
+ static public function SwitchBlocsPositions(EntityManager $entityManager, array $json, string $page): JsonResponse
+ {
+ if(isset($json['id1']) && is_int($json['id1']) && isset($json['id2']) && is_int($json['id2']) && isset($page)){
+ $model = new Model($entityManager);
+ $model->makeMenuAndPaths(); // true pour $model->findItsChildren();
+ $model->findUniqueNodeByName('main');
+ $model->findItsChildren();
+ $main = $model->getNode();
+ $main->sortChildren(true); // régénère les positions avant inversion
+
+ $bloc1 = null;
+ $bloc2 = null;
+ foreach($main->getChildren() as $child){
+ if($child->getId() === $json['id1']){
+ $bloc1 = $child;
+ break;
+ }
+ }
+ foreach($main->getChildren() as $child){
+ if($child->getId() === $json['id2']){
+ $bloc2 = $child;
+ break;
+ }
+ }
+
+ // inversion
+ $tmp = $bloc1->getPosition();
+ $bloc1->setPosition($bloc2->getPosition());
+ $bloc2->setPosition($tmp);
+
+ $entityManager->flush();
+ return new JsonResponse(['success' => true]);
+ }
+ else{
+ return new JsonResponse(['success' => false]);
+ }
+ }
+
+ static public function changeArticlesOrder(EntityManager $entityManager, array $json): JsonResponse
+ {
+ if(isset($json['id']) && isset($json['chrono_order'])){
+ $model = new Model($entityManager);
+ $model->findNodeById($json['id']);
+
+ if($json['chrono_order'] === 'chrono'){
+ $chrono_order = true;
+ }
+ elseif($json['chrono_order'] === 'antichrono'){
+ $chrono_order = false;
+ }
+ else{
+ return new JsonResponse(['success' => false]);
+ }
+ $model->getNode()->getNodeData()->setChronoOrder($chrono_order);
+ $entityManager->flush();
+
+ return new JsonResponse(['success' => true, 'chrono_order' => $json['chrono_order']]);
+ }
+ else{
+ return new JsonResponse(['success' => false]);
+ }
+ }
+
+ static public function changePresentation(EntityManager $entityManager, array $json): JsonResponse
+ {
+ if(isset($json['id']) && isset($json['presentation'])){
+ $model = new Model($entityManager);
+ $model->findNodeById($json['id']);
+
+ if(in_array($json['presentation'], array_keys(Presentation::PRESENTATIONS))){
+ $model->getNode()->getNodeData()->setPresentation($json['presentation']);
+ $entityManager->flush();
+
+ $response_data = ['success' => true, 'presentation' => $json['presentation']];
+ if($json['presentation'] === 'grid'){
+ $response_data['cols_min_width'] = $model->getNode()->getNodeData()->getColsMinWidth();
+ }
+ return new JsonResponse($response_data);
+ }
+ else{
+ return new JsonResponse(['success' => false]);
+ }
+ }
+ else{
+ return new JsonResponse(['success' => false]);
+ }
+ }
+ static public function changeColsMinWidth(EntityManager $entityManager, array $json): JsonResponse
+ {
+ if(isset($json['id']) && isset($json['cols_min_width'])){
+ $model = new Model($entityManager);
+ $model->findNodeById($json['id']);
+ $model->getNode()->getNodeData()->setColsMinWidth((int)$json['cols_min_width']); // attention conversion?
+
+ $entityManager->flush();
+ return new JsonResponse(['success' => true, 'cols_min_width' => $json['cols_min_width']]);
+ }
+ else{
+ return new JsonResponse(['success' => false]);
+ }
+ }
+ static public function changePaginationLimit(EntityManager $entityManager, array $json): JsonResponse
+ {
+ if(isset($json['id']) && isset($json['pagination_limit'])){
+ $model = new Model($entityManager);
+ $model->findNodeById($json['id']);
+ $old_limit = $model->getNode()->getNodeData()->getPaginationLimit() ?? 12;
+ $model->getNode()->getNodeData()->setPaginationLimit((int)$json['pagination_limit']); // attention conversion?
+
+ $entityManager->flush();
+
+ return new JsonResponse(['success' => true, 'old_limit' => $old_limit, 'new_limit' => $json['pagination_limit']]);
+ }
+ else{
+ return new JsonResponse(['success' => false]);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Controller/UserController.php b/src/Controller/UserController.php
new file mode 100644
index 0000000..812a2da
--- /dev/null
+++ b/src/Controller/UserController.php
@@ -0,0 +1,237 @@
+ UserController devrait se limiter à gérer des requêtes et réponses (changement transparent pour le routeur)
+il instancierait un classe correspondant au formulaire (prend POST et SESSION) et une classe "métier" UserService
+=> UserService contiendrait des méthodes utilisant l'objet formulaire pour agir sur la BDD
+=> les formulaires peuvent tenir dans une classe "UserUpdateForm" avec des stratégies ou plusieurs, les données y sont validées
+=> il est aussi possible de découper UserController en contrôleurs par fonctionnalité:
+Auth (connexion, deconnexion), User (infos, choix photo), Account (créer, supprimer compte), Avatar (upload photo...)
+*/
+
+declare(strict_types=1);
+
+namespace App\Controller;
+
+use Symfony\Component\HttpFoundation\RedirectResponse;
+use Doctrine\ORM\EntityManager;
+use App\Entity\User;
+use App\Entity\Log;
+use App\Service\FormValidation;
+use App\Service\ValidationStrategy;
+use App\Service\URL;
+use App\Service\Backup;
+use App\Service\EmailService;
+
+class UserController
+{
+ // account
+ static public function createAdminUser(EntityManager $entityManager): RedirectResponse
+ {
+ unset($_SESSION['user']);
+
+ $form = new FormValidation($_POST, ValidationStrategy::CreateUser);
+
+ $url = new URL;
+ $error = '';
+ if($form->validate()){
+ $password = password_hash($_POST['password'], PASSWORD_DEFAULT);
+ $user = new User($_POST['login'], 'admin', $password);
+ $entityManager->persist($user);
+ $entityManager->flush();
+ }
+ else{
+ $error = $form->getError();
+ }
+
+ if(!empty($error)){
+ $url->addParams(['error' => $error]);
+ }
+
+ return new RedirectResponse((string)$url);
+ }
+
+ // account
+ //static public function createUser(EntityManager $entityManager){}
+
+ // auth
+ static public function connect(EntityManager $entityManager): RedirectResponse
+ {
+ $url = new URL;
+ if(IS_ADMIN){ // déjà connecté?
+ return new RedirectResponse((string)$url);
+ }
+ unset($_SESSION['user']);
+
+ $form = new FormValidation($_POST, ValidationStrategy::Connection);
+
+ $error = '';
+ if($form->validate()){
+ // à mettre dans une classe métier UserService, Authentication, AuthService?
+ $user = self::getUserByName($_POST['login'], $entityManager);
+ if(!empty($user) && $_POST['login'] === $user->getLogin() && password_verify($_POST['password'], $user->getPassword())){
+ $log = new Log(true);
+
+ // protection fixation de session, si l'attaquant crée un cookie de session, il est remplacé
+ session_regenerate_id(true);
+ $_SESSION['user']['id'] = $user->getId();
+ $_SESSION['user']['username'] = $user->getLogin();
+ $_SESSION['user']['role'] = $user->getRole();
+
+ EmailService::cleanEmails($entityManager);
+
+ try{
+ if(file_exists('../var/backups')){
+ Backup::mySQLdump($entityManager, 'auto'); // créer un nouveau backup, sauf si les dossiers nécessaires sont encore à créer
+ }
+ }
+ catch(\RuntimeException $e){
+ echo '';
+ }
+
+ $url->addParams(isset($_GET['from']) ? ['page' => $_GET['from']] : []);
+ isset($_GET['id']) ? $url->addParams(['id' => $_GET['id']]) : '';
+ }
+ else
+ {
+ $log = new Log(false);
+ $error = 'bad_login_or_password';
+ }
+ $entityManager->persist($log);
+ $entityManager->flush();
+ }
+ else{
+ $error = $form->getError();
+ }
+
+ if(!empty($error)){
+ sleep(1); // défense basique à la force brute
+ $url->addParams(['page' => 'connection']);
+ isset($_GET['from']) ? $url->addParams(['from' => $_GET['from']]) : null;
+ isset($_GET['id']) ? $url->addParams(['id' => $_GET['id']]) : null;
+ $url->addParams(['error' => $error]);
+ }
+
+ return new RedirectResponse((string)$url);
+ }
+
+ // auth
+ static public function disconnect(): RedirectResponse
+ {
+ // nettoyage complet
+ unset($_SESSION['user']); // mémoire vive
+ session_destroy(); // fichier côté serveur
+ setcookie('PHPSESSID', '', time() - 86400, '/'); // cookie de session
+
+ // retour même page
+ $url = new URL;
+ isset($_GET['from']) ? $url->addParams(['page' => $_GET['from']]) : '';
+ isset($_GET['id']) ? $url->addParams(['id' => $_GET['id']]) : '';
+ return new RedirectResponse((string)$url);
+ }
+
+ // user
+ static public function updateUsername(EntityManager $entityManager): RedirectResponse
+ {
+ if(!IS_ADMIN){ // superflux, fait dans le routeur
+ return self::disconnect();
+ }
+
+ $url = new URL(['page' => 'user_edit']);
+ isset($_GET['from']) ? $url->addParams(['from' => $_GET['from']]) : null;
+
+ $form = new FormValidation($_POST, ValidationStrategy::UsernameUpdate);
+
+ $error = '';
+ if($form->validate()){
+ // à mettre dans une classe métier UserService?
+ $user = self::getUserByName($_POST['login'], $entityManager);
+ if(password_verify($_POST['password'], $user->getPassword())){
+ $user->setLogin($_POST['new_login']);
+ $entityManager->flush();
+ $_SESSION['user']['username'] = $_POST['new_login'];
+
+ $url->addParams(['success_username' => 'new_login']);
+ $error = '';
+ }
+ else{
+ $error = 'bad_login_or_password';
+ }
+ }
+ else{
+ $error = $form->getError();
+ }
+
+ if(!empty($error)){
+ sleep(1);
+ $url->addParams(['error_username' => $error]);
+ }
+ return new RedirectResponse((string)$url);
+ }
+
+ // user
+ static public function updatePassword(EntityManager $entityManager): RedirectResponse
+ {
+ if(!IS_ADMIN){ // superflux, fait dans le routeur
+ return self::disconnect();
+ }
+
+ $url = new URL(['page' => 'user_edit']);
+ isset($_GET['from']) ? $url->addParams(['from' => $_GET['from']]) : null;
+
+ $form = new FormValidation($_POST, ValidationStrategy::PasswordUpdate);
+
+ $error = '';
+ if($form->validate()){
+ // à mettre dans une classe métier UserService?
+ $user = self::getUserByName($_POST['login'], $entityManager);
+ if(password_verify($_POST['password'], $user->getPassword())){
+ $new_password = password_hash($_POST['new_password'], PASSWORD_DEFAULT);
+ $user->setPassword($new_password);
+ $entityManager->flush();
+
+ $url->addParams(['success_password' => 'new_password']);
+ $error = '';
+ }
+ else{
+ $error = 'bad_login_or_password';
+ }
+ }
+ else{
+ $error = $form->getError();
+ }
+
+ if(!empty($error)){
+ sleep(1);
+ $url->addParams(['error_password' => $error]);
+ }
+ return new RedirectResponse((string)$url);
+ }
+
+ // dans une classe mère ou un trait après découpage de UserController?
+ static private function getUserByName(string $login, EntityManager $entityManager): ?User
+ {
+ $users = $entityManager->getRepository(User::class)->findBy(['login' => $login]);
+ foreach($users as $user){
+ if($user->getLogin() === $login){
+ return $user;
+ }
+ }
+ return null;
+ }
+
+ static public function getUserById(int $id, EntityManager $entityManager): ?User
+ {
+ return $entityManager->find(User::class, $id);
+ }
+
+ // dans une classe Form?
+ // erreurs à la création des mots de passe
+ static private function removeSpacesTabsCRLF(string $chaine): string
+ {
+ $cibles = [' ', "\t", "\n", "\r"]; // doubles quotes !!
+ return(str_replace($cibles, '', $chaine));
+ }
+}
\ No newline at end of file
diff --git a/src/Controller/ViewController.php b/src/Controller/ViewController.php
new file mode 100644
index 0000000..1f13861
--- /dev/null
+++ b/src/Controller/ViewController.php
@@ -0,0 +1,70 @@
+query->has('mode') && $request->query->get('mode') === 'page_modif'
+ && !in_array(CURRENT_PAGE, self::SPECIAL_PAGES))
+ {
+ MainBuilder::$modif_mode = true;
+ }
+ // page article: mode création et erreurs d'id
+ if(CURRENT_PAGE === 'article'){
+ if(IS_ADMIN){
+ if(!$request->query->has('id')){
+ return new RedirectResponse((string)new URL(['page' => $request->query->get('from') ?? '']));
+ }
+ else{
+ // mode création d'article
+ // l'id du bloc et 'from=' sont vérifiés dans ArticleController::editorSubmit
+ if($request->query->get('id')[0] === 'n' && $request->query->has('from') && !empty($request->query->get('from'))){
+ NewBuilder::$new_article_mode = true;
+ }
+ }
+ }
+ elseif($request->query->get('id')[0] === 'n'){ // accès page nouvelle article interdit sans être admin
+ return new RedirectResponse((string)new URL(['page' => $request->query->get('from') ?? '']));
+ }
+ }
+ // pas de else, l'id dans l'URL n'a pas d'effet ailleurs
+
+
+ /* 2/ accès au modèle */
+ $model = new Model($entityManager);
+ $model->makeMenuAndPaths();
+ $model->getWholePageData($request);
+
+
+ /* 3/ 2ème contrôle des paramètres avec les données récupérées */
+
+ // article non trouvé en BDD
+ if(CURRENT_PAGE === 'article' && !IS_ADMIN && $model->getNode()->getNodeByName('main')->getAdoptedChild() === null){
+ return new RedirectResponse((string)new URL(['page' => $request->query->get('from') ?? '']));
+ }
+
+
+ /* 4/ construction de la page avec builders et vues */
+ return new Response((new ViewDirector)->buildHTML($model->getNode()));
+ }
+}
\ No newline at end of file
diff --git a/src/Model/DatabaseSettingUp.php b/src/Model/DatabaseSettingUp.php
new file mode 100644
index 0000000..725eaed
--- /dev/null
+++ b/src/Model/DatabaseSettingUp.php
@@ -0,0 +1,157 @@
+getRepository(AppMetadata::class)->find('installed');
+ return !$metadata || $metadata->getValue() !== '1';
+ }
+
+ // protection 2, qui vérifie vraiment que les tables concernées sont vides
+ static private function areTablesEmpty(EntityManager $entityManager): bool
+ {
+ $empty = true;
+ $entities = ['Page', 'Node', 'NodeData'];
+ foreach($entities as $entity){
+ $entity = 'App\Entity\\' . $entity; // nécéssaire quand on insère le nom avec une variable
+
+ if($entityManager
+ ->createQuery("SELECT e FROM $entity e")
+ ->setMaxResults(1)
+ ->getOneOrNullResult()){
+ $empty = false;
+ }
+ }
+
+ // cas anormal détecté, on remet en place la clé "installed"
+ if(!$empty){
+ self::preventReinstallation($entityManager);
+ }
+
+ return $empty;
+ }
+
+ static private function fillStartingDatabase(EntityManager $entityManager): void
+ {
+ /* -- table page -- */
+ // paramètres: name_page, end_of_path, reachable, in_menu, hidden, position, parent
+ $accueil = new Page('Accueil', 'accueil', "Page d'accueil", true, true, false, 1, NULL);
+ $article = new Page('Article', 'article', "", true, false, false, NULL, NULL);
+ $connection = new Page('Connexion', 'connection', "Connexion", true, false, false, NULL, NULL);
+ $my_account = new Page('Mon compte', 'user_edit', "Mon compte", true, false, false, NULL, NULL);
+ $menu_paths = new Page("Menu et chemins", 'menu_paths', "Menu et chemins", true, false, false, NULL, NULL);
+ $menu_paths->addCSS('menu');
+ $menu_paths->addJS('menu');
+ $new_page = new Page('Nouvelle page', 'new_page', "Nouvelle page", true, false, false, NULL, NULL);
+ $new_page->addCSS('new_page');
+ $new_page->addJS('new_page');
+ $emails = new Page("Courriels", 'emails', "Consulter les courriels en base de données", true, false, false, NULL, NULL);
+ $emails->addCSS('show_emails');
+ $emails->addJS('form');
+ $maintenance = new Page("Maintenance", 'maintenance', "Opérations de maintenance, installation, sauvegarde, restauration", true, false, false, NULL, NULL);
+ $maintenance->addCSS('maintenance');
+ $maintenance->addJS('maintenance');
+
+ /* -- table node -- */
+ // paramètres: name_node, article_timestamp, attributes, position, parent, page, article
+ $head = new Node('head', 1, NULL, NULL, NULL);
+ $header = new Node('header', 2, NULL, NULL, NULL);
+ $nav = new Node('nav', 1, $header, NULL, NULL);
+ $main = new Node('main', 3, NULL, NULL, NULL);
+ $footer = new Node('footer', 4, NULL, NULL, NULL);
+ $breadcrumb = new Node('breadcrumb', 2, $header, NULL, NULL);
+ $login = new Node('login', 1, $main, $connection, NULL);
+ $user_edit = new Node('user_edit', 1, $main, $my_account, NULL);
+ $bloc_edit_menu = new Node('menu', 1, $main, $menu_paths, NULL);
+ $bloc_new_page = new Node('new_page', 1, $main, $new_page, NULL);
+ $bloc_emails = new Node('show_emails', 1, $main, $emails, NULL);
+ $bloc_maintenance = new Node('maintenance', 1, $main, $maintenance, NULL);
+
+ /* -- table node_data -- */
+ // paramètres: data, node, images
+ $head_data = new NodeData([], $head);
+ $header_data = new NodeData([], $header);
+ $footer_data = new NodeData([], $footer);
+ $emails_data = new NodeData([], $bloc_emails);
+
+ /* -- table page -- */
+ $entityManager->persist($accueil);
+ $entityManager->persist($article);
+ $entityManager->persist($connection);
+ $entityManager->persist($my_account);
+ $entityManager->persist($menu_paths);
+ $entityManager->persist($new_page);
+ $entityManager->persist($emails);
+ $entityManager->persist($maintenance);
+
+ /* -- table node -- */
+ $entityManager->persist($head);
+ $entityManager->persist($header);
+ $entityManager->persist($nav);
+ $entityManager->persist($main);
+ $entityManager->persist($footer);
+ $entityManager->persist($breadcrumb);
+ $entityManager->persist($login);
+ $entityManager->persist($user_edit);
+ $entityManager->persist($bloc_edit_menu);
+ $entityManager->persist($bloc_new_page);
+ $entityManager->persist($bloc_emails);
+ $entityManager->persist($bloc_maintenance);
+
+ /* -- table node_data -- */
+ $entityManager->persist($head_data);
+ $entityManager->persist($header_data);
+ $entityManager->persist($footer_data);
+ $entityManager->persist($emails_data);
+
+ $entityManager->flush();
+ }
+
+ // met en place la protection
+ static private function preventReinstallation(EntityManager $entityManager): void
+ {
+ $metadata = $entityManager->getRepository(AppMetadata::class)->find('installed');
+ if($metadata){
+ $metadata->setValue('1');
+ }
+ else{
+ $metadata = new AppMetadata('installed', '1');
+ $entityManager->persist($metadata);
+ }
+ $entityManager->flush();
+ }
+}
\ No newline at end of file
diff --git a/src/Model/Entity/AppMetadata.php b/src/Model/Entity/AppMetadata.php
new file mode 100644
index 0000000..b7d6923
--- /dev/null
+++ b/src/Model/Entity/AppMetadata.php
@@ -0,0 +1,46 @@
+key_name = $key;
+ $this->value = $value;
+ }
+
+ public function getKey(): string
+ {
+ return $this->key_name;
+ }
+ public function getValue(): string
+ {
+ return $this->value;
+ }
+
+ public function setKey(string $key): void
+ {
+ $this->key_name = $key;
+ }
+ public function setValue(string $value): void
+ {
+ $this->value = $value;
+ }
+}
\ No newline at end of file
diff --git a/src/Model/Entity/Article.php b/src/Model/Entity/Article.php
new file mode 100644
index 0000000..588ebd6
--- /dev/null
+++ b/src/Model/Entity/Article.php
@@ -0,0 +1,94 @@
+ 'CURRENT_TIMESTAMP'], unique: true)]
+ private ?\DateTime $date_time; // le type datetime de doctrine convertit en type \DateTime de PHP
+
+ #[ORM\Column(type: "text")] // "string" => varchar(255) => risque HTML non valide
+ private string $title;
+
+ #[ORM\Column(type: "text")]
+ private string $preview; // une simple textarea
+
+ #[ORM\Column(type: "text")]
+ private string $content; // de l'éditeur html
+
+ #[ORM\ManyToMany(targetEntity: Image::class, inversedBy: "article")] // cascade: ['remove'] = très dangereux!
+ #[ORM\JoinTable(
+ name: TABLE_PREFIX . "article_image",
+ joinColumns: [new ORM\JoinColumn(name: "article_id", referencedColumnName: "id_article", onDelete: "CASCADE")], // onDelete: "CASCADE": très utile
+ inverseJoinColumns: [new ORM\JoinColumn(name: "image_id", referencedColumnName: "id_image", onDelete: "CASCADE")]
+ )]
+ private Collection $images;
+
+ public function __construct(string $content, ?\DateTime $date_time = null, string $title = '', string $preview = '')
+ {
+ $this->date_time = $date_time;
+ $this->title = $title;
+ $this->preview = $preview;
+ $this->content = $content;
+ $this->images = new ArrayCollection(); // initialisation nécessaire
+ }
+
+ public function getDateTime(): \DateTime
+ {
+ return $this->date_time;
+ }
+ public function setDateTime(\DateTime $date_time): void
+ {
+ $this->date_time = $date_time;
+ }
+ public function getTimestamp(): int
+ {
+ return $this->date_time->getTimestamp();
+ }
+ public function getTitle(): string
+ {
+ return $this->title;
+ }
+ public function setTitle(string $title): void
+ {
+ $this->title = $title;
+ }
+ public function getPreview(): string
+ {
+ return $this->preview;
+ }
+ public function setPreview(string $preview): void
+ {
+ $this->preview = $preview;
+ }
+ public function getContent(): string
+ {
+ return $this->content;
+ }
+ public function setContent(string $data): void
+ {
+ $this->content = $data;
+ }
+
+ public function getImages(): Collection
+ {
+ return $this->images;
+ }
+}
diff --git a/src/Model/Entity/Asset.php b/src/Model/Entity/Asset.php
new file mode 100644
index 0000000..51e21fe
--- /dev/null
+++ b/src/Model/Entity/Asset.php
@@ -0,0 +1,68 @@
+file_name = $name;
+ $this->mime_type = $mime_type;
+ $this->hash = $hash;
+ }
+
+ public function getFileName(): string
+ {
+ return $this->file_name;
+ }
+ public function setFileName(string $name): void
+ {
+ $this->file_name = $name;
+ }
+ public function getMimeType(): string
+ {
+ return $this->mime_type;
+ }
+ public function setMimeType(string $mime_type): void
+ {
+ $this->mime_type = $mime_type;
+ }
+ public function getHash(): string
+ {
+ return $this->hash;
+ }
+
+ public function getAssetEmployments(): Collection
+ {
+ return $this->asset_employment;
+ }
+}
diff --git a/src/Model/Entity/AssetEmployment.php b/src/Model/Entity/AssetEmployment.php
new file mode 100644
index 0000000..e608af0
--- /dev/null
+++ b/src/Model/Entity/AssetEmployment.php
@@ -0,0 +1,54 @@
+ ressource unique" (paires node_data/role uniques)
+// doctrine gère mal les clés primaires triples, j'ai donc choisi une clé primaire double node_data_id/role
+
+declare(strict_types=1);
+
+namespace App\Entity;
+
+use Doctrine\ORM\Mapping as ORM;
+
+#[ORM\Entity]
+#[ORM\Table(name: TABLE_PREFIX . 'asset_employment')]
+class AssetEmployment
+{
+ // clé primaire double
+ #[ORM\Id]
+ #[ORM\ManyToOne(targetEntity: NodeData::class, inversedBy: 'asset_employment')]
+ #[ORM\JoinColumn(name: 'node_data_id', referencedColumnName: 'id_node_data', onDelete: 'CASCADE')]
+ private NodeData $node_data;
+
+ #[ORM\Id]
+ #[ORM\Column(type: 'string', length: 50)]
+ private string $role;
+
+ #[ORM\ManyToOne(targetEntity: Asset::class, inversedBy: 'asset_employment')]
+ #[ORM\JoinColumn(name: 'asset_id', referencedColumnName: 'id_asset', onDelete: 'CASCADE')]
+ private Asset $asset;
+
+ public function __construct(NodeData $node_data, Asset $asset, string $role){
+ $this->node_data = $node_data;
+ $this->asset = $asset;
+ $this->role = $role;
+ }
+
+ /*public function getNodeData(): NodeData
+ {
+ return $this->node_data;
+ }*/
+ public function getAsset(): Asset
+ {
+ return $this->asset;
+ }
+ public function setAsset(Asset $asset): self
+ {
+ $this->asset = $asset;
+ return $this;
+ }
+ public function getRole(): string
+ {
+ return $this->role;
+ }
+}
diff --git a/src/Model/Entity/Email.php b/src/Model/Entity/Email.php
new file mode 100644
index 0000000..8ad66fd
--- /dev/null
+++ b/src/Model/Entity/Email.php
@@ -0,0 +1,131 @@
+ 'CURRENT_TIMESTAMP'])]
+ private \DateTime $date_time;
+
+ #[ORM\Column(type: 'boolean')]
+ private bool $is_sensitive; // "sensitive" tout court est un mot réservé
+
+ #[ORM\Column(type: 'datetime', options: ['default' => 'CURRENT_TIMESTAMP'])]
+ private \DateTime $last_contact_date;
+
+ #[ORM\Column(type: 'datetime', nullable: true)]
+ private ?\DateTime $is_sensitive_since;
+
+ #[ORM\ManyToOne(targetEntity: EmailForm::class)]
+ #[ORM\JoinColumn(name: "email_form_id", referencedColumnName: "id_email_form", nullable: true)]
+ private ?EmailForm $email_form;
+
+ public function __construct(string $sender_name, string $sender_address, string $recipient, string $content, EmailForm $email_form, bool $sensitive = false){
+ $this->sender_name = strtolower($sender_name);
+ $this->sender_address = strtolower($sender_address);
+ $this->recipient = strtolower($recipient);
+ $this->content = $content;
+ $this->date_time = new \DateTime;
+ $this->last_contact_date = new \DateTime;
+ $this->email_form = $email_form;
+ $this->makeSensitive($sensitive);
+ }
+
+ public function getId(): int
+ {
+ return $this->id_email;
+ }
+ public function getSenderName(): string
+ {
+ return $this->sender_name;
+ }
+ public function getSenderAddress(): string
+ {
+ return $this->sender_address;
+ }
+ public function getRecipient(): string
+ {
+ return $this->recipient;
+ }
+ public function getContent(): string
+ {
+ return $this->content;
+ }
+ public function getDateTime(): \DateTime
+ {
+ return $this->date_time;
+ }
+ /*public function getLastContactDate(): \DateTime
+ {
+ return $this->last_contact_date;
+ }*/
+ public function isSensitive(): bool
+ {
+ return $this->is_sensitive;
+ }
+ public function isSensitiveSince(): ?\DateTime
+ {
+ return $this->is_sensitive_since;
+ }
+
+ public function makeSensitive(bool $sensitive = true): void
+ {
+ $this->is_sensitive = $sensitive;
+ if($sensitive && $this->is_sensitive_since === null){
+ $this->is_sensitive_since = new \DateTime();
+ }
+ }
+
+ public function updateLastContactDate(): void
+ {
+ $this->last_contact_date = new \DateTime;
+ }
+
+ public function getDeletionDate(): \DateTime // utilise une durée de conservation $period qui est propre au bloc formulaire (à son EmailForm)
+ {
+ // tests appliqués:
+ // => e-mail associé à un formulaire?
+ // => ce formulaire dispose d'une durée de stockage spécifique?
+ // => cette donnée est un entier > 0
+ $key = $this->is_sensitive ? 'retention_period_sensible' : 'retention_period';
+ $period = $this->email_form ? (int)($this->email_form->getData()[$key] ?? null) : null;
+
+ $default = $this->is_sensitive ? self::DEFAULT_RETENTION_PERIOD_SENSITIVE : self::DEFAULT_RETENTION_PERIOD;
+ $period = ($period === null || $period <= 0) ? $default : $period;
+
+ $date = clone ($this->is_sensitive ? $this->is_sensitive_since : $this->last_contact_date); // erreur si "sensible" mais sans date disponible (pas censé arriver)
+ return $date->modify('+ ' . (string)$period . ' month');
+ }
+}
\ No newline at end of file
diff --git a/src/Model/Entity/EmailForm.php b/src/Model/Entity/EmailForm.php
new file mode 100644
index 0000000..5c54b86
--- /dev/null
+++ b/src/Model/Entity/EmailForm.php
@@ -0,0 +1,57 @@
+data = $data;
+ $this->node = $node;
+ }
+
+ public function getId(): int
+ {
+ return $this->id_email_form;
+ }
+
+ // getData et updateData sont indentiques au code dans NodeData
+ // plutôt qu'une interface, pourquoi pas une classe abstraite? ou peut-être un trait?
+ public function getData(): array
+ {
+ return $this->data;
+ }
+ public function updateData(string $key, string|int|bool|array $value = ''): void
+ {
+ if($value !== ''){
+ $this->data[$key] = $value;
+ }
+ // si $value est vide, supprime la clé
+ elseif(isset($this->data[$key])){
+ unset($this->data[$key]);
+ }
+ }
+
+ public function setNode(?Node $node): void
+ {
+ $this->node = $node;
+ }
+}
\ No newline at end of file
diff --git a/src/Model/Entity/Event.php b/src/Model/Entity/Event.php
new file mode 100644
index 0000000..3b81da7
--- /dev/null
+++ b/src/Model/Entity/Event.php
@@ -0,0 +1,129 @@
+ 255)
+
+ #[ORM\Column(type: 'text')]
+ private string $description = '';
+ // chatgpt: Dans un contexte API/Front comme FullCalendar,
+ // préférer une chaîne vide à une varaible "null" peut être plus pratique,
+ // car ça évite des contrôles côté JS.
+
+ #[ORM\Column(type: 'datetime')] // chatgpt: Doctrine suppose UTC si pas de configuration spécifique
+ private \DateTimeInterface $start; // typage possible avec une interface,
+ //chatgpt: choix \DateTime par défaut, autorise \DateTimeImmutable
+
+ #[ORM\Column(type: 'datetime')]
+ private \DateTimeInterface $end;
+
+ #[ORM\Column(type: 'boolean')]
+ private bool $all_day;
+
+ #[ORM\Column(type: 'string', length: 7, nullable: true)]
+ private ?string $color = null;
+
+ public function __construct(array $json){
+ $this->securedUpdateFromJSON($json);
+ }
+
+ public function securedUpdateFromJSON(array $json): void
+ {
+ $this->title = htmlspecialchars($json['title']);
+ $this->description = htmlspecialchars($json['description']);
+ try{
+ $this->start = new \Datetime($json['start']);
+ $this->end = new \Datetime($json['end']);
+ }
+ catch(\Exception $e){
+ throw new \InvalidArgumentException('Bad date input');
+ }
+ $all_day = filter_var($json['allDay'] ?? null, FILTER_VALIDATE_BOOLEAN, FILTER_NULL_ON_FAILURE);
+ if(!is_bool($all_day)){
+ throw new \InvalidArgumentException('Bad checkbox input');
+ }
+ $this->all_day = $all_day;
+ $this->color = isset($json['color']) ? htmlspecialchars($json['color']) : null;
+ }
+
+ public function getId(): int
+ {
+ return $this->id;
+ }
+
+ public function getTitle(): string
+ {
+ return $this->title;
+ }
+ /*public function setTitle(string $title): self
+ {
+ $this->title = $title;
+ return $this;
+ }*/
+
+ public function getDescription(): string
+ {
+ return $this->description;
+ }
+ /*public function setDescription(string $description = ''): self
+ {
+ $this->description = $description;
+ return $this;
+ }*/
+
+ public function getStart(): \DateTimeInterface
+ {
+ return $this->start;
+ }
+ /*public function setStart(\DateTimeInterface $start): self
+ {
+ $this->start = $start;
+ return $this;
+ }*/
+
+ public function getEnd(): \DateTimeInterface
+ {
+ return $this->end;
+ }
+ /*public function setEnd(\DateTimeInterface $end): self
+ {
+ $this->end = $end;
+ return $this;
+ }*/
+
+ public function isAllDay(): bool
+ {
+ return $this->all_day;
+ }
+ /*public function setAllDay(bool $all_day): self
+ {
+ $this->all_day = $all_day;
+ return $this;
+ }*/
+
+ public function getColor(): ?string
+ {
+ return $this->color;
+ }
+ /*public function setColor(?string $color): self
+ {
+ $this->color = $color;
+ return $this;
+ }*/
+}
diff --git a/src/Model/Entity/Image.php b/src/Model/Entity/Image.php
new file mode 100644
index 0000000..06230ce
--- /dev/null
+++ b/src/Model/Entity/Image.php
@@ -0,0 +1,89 @@
+ Validation du type de fichier : On vérifie que le fichier est bien une image en utilisant le type MIME. On peut aussi vérifier la taille du fichier.
+ => Création d'un répertoire structuré : On génère un chemin dynamique basé sur la date (uploads/2024/12/24/) pour organiser les images.
+ => Génération d'un nom de fichier unique : On utilise uniqid() pour générer un nom unique et éviter les conflits de nom.
+ => Déplacement du fichier sur le serveur : Le fichier est déplacé depuis son emplacement temporaire vers le répertoire uploads/.
+ => Enregistrement dans la base de données : On enregistre les informations de l'image dans la base de données. */
+
+ #[ORM\ManyToMany(targetEntity: Article::class, mappedBy: "images")]
+ private Collection $article;
+
+ public function __construct(string $name, ?string $path, ?string $path_mini, string $mime_type, string $alt)
+ {
+ $this->file_name = $name;
+ $this->file_path = $path;
+ $this->file_path_mini = $path_mini;
+ $this->mime_type = $mime_type;
+ $this->alt = $alt;
+ }
+
+ public function getFileName(): string
+ {
+ return $this->file_name;
+ }
+ public function getFilePath(): string
+ {
+ return $this->file_path;
+ }
+ public function getFilePathMini(): string
+ {
+ return $this->file_path_mini;
+ }
+ public function getAlt(): string
+ {
+ return $this->alt;
+ }
+
+
+ // pour ViewBuilderController?
+ /*public function displayImage($imageId): void
+ {
+ //$imageId = 1; // Exemple d'ID d'image
+ $stmt = $pdo->prepare("SELECT file_path FROM images WHERE id = ?");
+ $stmt->execute([$imageId]);
+ $image = $stmt->fetch();
+
+ if ($image) {
+ echo "
+
+
+ ';
+ foreach($data as $entry){
+ $view .= 'date et heure (Greenwich)
+ connexion réussie
+
+ ';
+ }
+ $view .= '' . $entry->getFormatedDate() . '
+ ' . ($entry->getSuccess() ? 'oui' : 'non') . '
+ ";
+ } else {
+ echo "Image non trouvée.";
+ }
+ }*/
+}
diff --git a/src/Model/Entity/Log.php b/src/Model/Entity/Log.php
new file mode 100644
index 0000000..b1cc168
--- /dev/null
+++ b/src/Model/Entity/Log.php
@@ -0,0 +1,38 @@
+ 'CURRENT_TIMESTAMP'])] // CURRENT_TIMESTAMP "inutile", date générée par PHP
+ private \DateTime $date_time; // type datetime de doctrine <=> type \DateTime de PHP
+
+ #[ORM\Column(type: "boolean")]
+ private bool $success;
+
+ public function __construct(bool $success){
+ $this->date_time = new \DateTime;
+ $this->success = $success;
+ }
+
+ public function getFormatedDate(): string
+ {
+ return $this->date_time->format('d/m/Y à H\hi');
+ }
+ public function getSuccess(): bool
+ {
+ return $this->success;
+ }
+}
diff --git a/src/Model/Entity/Node.php b/src/Model/Entity/Node.php
new file mode 100644
index 0000000..07666c3
--- /dev/null
+++ b/src/Model/Entity/Node.php
@@ -0,0 +1,167 @@
+name_node = $name;
+ $this->position = $position;
+ $this->parent = $parent;
+ $this->page = $page;
+ $this->article = $article;
+ }
+
+ // pfff...
+ public function getId(): ?int
+ {
+ return $this->id_node;
+ }
+ public function getName(): string
+ {
+ return $this->name_node;
+ }
+ /*public function setName(string $name): void
+ {
+ $this->name_node = $name;
+ }*/
+
+ public function getParent(): ?self
+ {
+ return $this->parent;
+ }
+ /*public function setParent(?self $parent): void
+ {
+ $this->parent = $parent;
+ }*/
+ public function getPosition(): int
+ {
+ return $this->position;
+ }
+ public function setPosition(int $position): void
+ {
+ $this->position = $position;
+ }
+ public function getPage(): Page
+ {
+ return $this->page;
+ }
+ /*public function setPage(Page $page): void
+ {
+ $this->page = $page;
+ }*/
+
+ // utiliser l'interface de NodeData et EmailForm
+ public function getArticle(): Article
+ {
+ return $this->article;
+ }
+ /*public function setArticle(Article $article): void
+ {
+ $this->article = $article;
+ }*/
+
+ // une interface serait cool!
+ // OU possible un "héritage doctrine", faire en sorte que Node soit la mère de NodeData, EmailForm, Article...
+ // une seule instance est matérialisée par plusieurs tables en même temps
+ public function getNodeData(): NodeData|EmailForm|Presentation|null
+ {
+ // pour du polymorphisme (une unique variable $node_data): opter pour l'héritage
+ return match($this->name_node){
+ 'form' => $this->email_form,
+ 'post_block', 'news_block' => $this->presentation,
+ default => $this->node_data, // inclut 'calendar'
+ };
+ }
+ public function getChildren(): array
+ {
+ return $this->children;
+ }
+ public function getNodeByName(string $name): ?Node
+ {
+ foreach($this->children as $child){
+ if($child->getName() === $name){
+ return $child;
+ }
+ }
+ return null;
+ }
+ public function addChild(self $child): void
+ {
+ $this->children[] = $child;
+
+ if(!Presentation::hasPresentation($this->getName())){ // post_block et news_block ont leurs enfants ordonnés avec ORDER BY
+ $this->sortChildren(false);
+ }
+ }
+ public function removeChild(self $child): void
+ {
+ foreach($this->children as $key => $object){
+ if($object->getId() === $child->getId()){
+ unset($this->children[$key]);
+ }
+ break;
+ }
+ $this->children = array_values($this->children); // réindexer pour supprimer la case vide
+ }
+
+ public function getAdoptedChild(): ?self // peut renvoyer null
+ {
+ return $this->adopted;
+ }
+ public function setAdoptedChild(self $child): void
+ {
+ $this->adopted = $child;
+ }
+}
diff --git a/src/Model/Entity/NodeData.php b/src/Model/Entity/NodeData.php
new file mode 100644
index 0000000..d81628d
--- /dev/null
+++ b/src/Model/Entity/NodeData.php
@@ -0,0 +1,102 @@
+ noeud "show_emails"
+
+ public function __construct(array $data, Node $node, Collection $asset_employment = new ArrayCollection){
+ $this->data = $data;
+ $this->node = $node;
+ $this->asset_employment = $asset_employment;
+ }
+
+ public function getId(): int
+ {
+ return $this->id_node_data;
+ }
+ public function getData(): array
+ {
+ return $this->data;
+ }
+ /*public function setData(array $data): void // entrée = tableau associatif
+ {
+ $this->data = $data;
+ }*/
+ public function updateData(string $key, string|int|bool|array $value = ''): void
+ {
+ if($value !== ''){
+ $this->data[$key] = $value;
+ }
+ // si $value est vide, supprime la clé
+ elseif(isset($this->data[$key])){
+ unset($this->data[$key]);
+ }
+ }
+
+ public function setNode(?Node $node): void
+ {
+ $this->node = $node;
+ }
+
+ public function getNodeDataAssets(): Collection
+ {
+ return $this->asset_employment;
+ }
+ public function getAssetEmploymentByRole(string $role): ?AssetEmployment
+ {
+ foreach($this->asset_employment as $nda){
+ if($nda->getRole() === $role){
+ return $nda;
+ }
+ }
+ return null;
+ }
+ public function getAssetByRole(string $role): ?Asset
+ {
+ $nda = $this->getAssetEmploymentByRole($role);
+ if($nda === null){
+ return null;
+ }
+ return $nda->getAsset() ?? null;
+ }
+
+ // pour affichage page Courriels
+ public function getEmails(): array // appelée dans ShowEmailsBuilder
+ {
+ return $this->emails;
+ }
+ public function setEmails(array $emails): void // appelée dans Model
+ {
+ $this->emails = $emails;
+ }
+}
\ No newline at end of file
diff --git a/src/Model/Entity/Page.php b/src/Model/Entity/Page.php
new file mode 100644
index 0000000..7cca3dc
--- /dev/null
+++ b/src/Model/Entity/Page.php
@@ -0,0 +1,222 @@
+name_page = $name;
+ $this->end_of_path = $eop;
+ $this->description = $description;
+ $this->reachable = $reachable;
+ $this->in_menu = $in_menu;
+ $this->hidden = $hidden;
+ $this->position = $position;
+ $this->parent = $parent;
+ $this->children = new ArrayCollection();
+ }
+
+ // getters/setters
+ public function getId(): int
+ {
+ return $this->id_page;
+ }
+ public function getPageName(): string
+ {
+ return $this->name_page;
+ }
+ public function setPageName(string $name): void
+ {
+ $this->name_page = $name;
+ }
+ public function getPagePath(): string
+ {
+ return $this->page_path;
+ }
+ public function setPagePath(string $path):void
+ {
+ $this->page_path = $path;
+ }
+ public function getEndOfPath(): string
+ {
+ return $this->end_of_path;
+ }
+ public function setEndOfPath(string $end_of_path):void
+ {
+ $this->end_of_path = $end_of_path;
+ }
+ public function getDescription(): string
+ {
+ return $this->description;
+ }
+ public function setDescription(string $description): void
+ {
+ $this->description = $description;
+ }
+
+ public function getCSS(): array
+ {
+ return array_merge(self::$default_css, $this->css ?? []);
+ }
+ public function addCSS(string $css): void
+ {
+ if(!in_array($css, $this->css ?? [])){
+ $this->css[] = $css;
+ }
+ }
+ public function removeCSS(string $css): void
+ {
+ // array_diff renvoie une copie du 1er tableau alégée des élements existants aussi dans le 2è, array_values réindexe
+ $this->css = array_values(array_diff($this->css, [$css]));
+ }
+ public function getJS(): array
+ {
+ return array_merge(self::$default_js, $this->js ?? []);
+ //UPDATE `nb_page` SET `js` = NULL WHERE JSON_EQUALS(`js`, '["main"]');
+ }
+ public function addJS(string $js): void
+ {
+ if(!in_array($js, $this->js ?? [])){
+ $this->js[] = $js;
+ }
+ }
+ public function removeJS(string $js): void
+ {
+ $this->js = array_values(array_diff($this->js, [$js]));
+ }
+
+ public function isReachable(): bool
+ {
+ return $this->reachable;
+ }
+ public function isInMenu(): bool
+ {
+ return $this->in_menu;
+ }
+ public function isHidden(): bool
+ {
+ return $this->hidden;
+ }
+ public function setHidden(bool $hidden): void
+ {
+ $this->hidden = $hidden;
+ }
+ public function getPosition(): ?int
+ {
+ return $this->position;
+ }
+ public function setPosition(int $position): void
+ {
+ $this->position = $position;
+ }
+ public function getParent(): ?Page
+ {
+ return $this->parent;
+ }
+ public function setParent(?Page $parent): void
+ {
+ $this->parent = $parent;
+ }
+ public function getChildren(): Collection
+ {
+ return $this->children;
+ }
+
+ // utilisée par $menu_path
+ public function fillChildrenPagePath(string $parent_path = ''): void
+ {
+ $this->page_path = $parent_path != '' ? $parent_path . '/' . $this->end_of_path : $this->end_of_path;
+ foreach($this->getChildren() as $page){
+ $page->fillChildrenPagePath($this->page_path);
+ }
+ }
+
+ public function addChild(self $child): void
+ {
+ $this->children[] = $child;
+ $this->sortChildren(false);
+ }
+ public function removeChild(self $child): void
+ {
+ $this->children->removeElement($child);
+ $this->children = new ArrayCollection(array_values($this->children->toArray())); // réindexer en passant par un tableau
+ $this->sortChildren(false);
+ }
+
+ public function findPageById(int $id): ?Page
+ {
+ $target = null;
+ foreach($this->children as $page){
+ if($page->getId() === $id){
+ return $page;
+ }
+ if(count($page->getChildren()) > 0){
+ $target = $page->findPageById($id);
+ if($target !== null){
+ return $target;
+ }
+ }
+ }
+ return $target;
+ }
+}
diff --git a/src/Model/Entity/Presentation.php b/src/Model/Entity/Presentation.php
new file mode 100644
index 0000000..f5e3502
--- /dev/null
+++ b/src/Model/Entity/Presentation.php
@@ -0,0 +1,135 @@
+data = $data;
+ $this->node = $node;
+ /*if(!empty($presentation) && $presentation === 'grid'){
+ $this->grid_cols_min_width = 250;
+ }
+ $this->chrono_order = $chrono_order ?? null;*/
+ }
+
+ /*public function getId(): int
+ {
+ return $this->id_presentation;
+ }*/
+
+ public function setNode(?Node $node): void
+ {
+ $this->node = $node;
+ }
+
+ // un trait pour ça
+ public function getData(): array
+ {
+ return $this->data;
+ }
+ public function updateData(string $key, string|int|bool|array $value = ''): void
+ {
+ if($value !== ''){
+ $this->data[$key] = $value;
+ }
+ // si $value est vide, supprime la clé
+ elseif(isset($this->data[$key])){
+ unset($this->data[$key]);
+ }
+ }
+
+ // spécifique aux blocs contenant des articles
+ public function getPresentation(): ?string
+ {
+ return $this->presentation;
+ }
+ public function setPresentation(string $presentation): void
+ {
+ $this->presentation = $presentation;
+ }
+ public function getColsMinWidth(): int
+ {
+ $default = 320; // pixels
+ return $this->grid_cols_min_width === null ? $default : $this->grid_cols_min_width;
+ }
+ public function setColsMinWidth(int $columns): void
+ {
+ $this->grid_cols_min_width = $columns;
+ }
+ public function getChronoOrder(): bool
+ {
+ return $this->chrono_order ?? false;
+ }
+ public function setChronoOrder(bool $reverse_order): void
+ {
+ $this->chrono_order = $reverse_order;
+ }
+
+ public function getPaginationLimit(): ?int
+ {
+ $default = 12; // si 0 pas de pagination, 12 rend bien avec des grilles de 2, 3 ou 4 colonnes
+ return $this->pagination_limit === null ? $default : $this->pagination_limit;
+ }
+ public function setPaginationLimit(int $pagination_limit): void
+ {
+ $this->pagination_limit = $pagination_limit;
+ }
+ public function getNumberOfPages(): int
+ {
+ return $this->nb_pages;
+ }
+ public function setNumberOfPages(int $nb_pages): void
+ {
+ $this->nb_pages = $nb_pages;
+ }
+
+
+ /* -- blocs et présentations disponibles dans l'application -- */
+ const BLOCKS = ['post_block' => 'Articles libres', 'news_block' => 'Actualités',
+ //'galery' => 'Galerie',
+ 'calendar' => 'Calendrier', 'form' => 'Formulaire'
+ ];
+ const PRESENTATIONS = ['fullwidth' => 'Pleine largeur', 'grid' => 'Grille', 'mosaic' => 'Mosaïque'
+ //, 'carousel' => 'Carrousel'
+ ];
+ static public function hasPresentation(string $block): bool
+ {
+ return in_array($block, ['post_block', 'news_block']) ? true : false;
+ }
+}
\ No newline at end of file
diff --git a/src/Model/Entity/User.php b/src/Model/Entity/User.php
new file mode 100644
index 0000000..461a5bc
--- /dev/null
+++ b/src/Model/Entity/User.php
@@ -0,0 +1,67 @@
+login = $login;
+ $this->role = $role;
+ $this->password = $password;
+ }
+
+ public function getId(): int
+ {
+ return $this->id_user;
+ }
+
+ public function getLogin(): string
+ {
+ return $this->login;
+ }
+ public function setLogin(string $login): void
+ {
+ $this->login = $login;
+ }
+ public function getRole(): string
+ {
+ return $this->role;
+ }
+ public function setRole(string $role): void
+ {
+ $this->role = $role;
+ }
+ public function getPassword(): string
+ {
+ return $this->password;
+ }
+ public function setPassword(string $password): void
+ {
+ $this->password = $password;
+ }
+}
\ No newline at end of file
diff --git a/src/Model/EventDTO.php b/src/Model/EventDTO.php
new file mode 100644
index 0000000..b65176c
--- /dev/null
+++ b/src/Model/EventDTO.php
@@ -0,0 +1,53 @@
+id = $event->getId();
+ $this->title = $event->getTitle();
+ $this->description = $event->getDescription() ?? ''; // renvoie $event->getDescription() si existe et ne vaut pas "null"
+ $this->allDay = $event->isAllDay();
+ $this->color = $event->getColor();
+
+ if($this->allDay){
+ $this->start = $event->getStart()->format('Y-m-d');
+ $this->end = $event->getEnd()->format('Y-m-d');
+ }
+ else{
+ $this->start = $event->getStart()->format('Y-m-d\TH:i:s\Z');
+ $this->end = $event->getEnd()->format('Y-m-d\TH:i:s\Z');
+ }
+ }
+
+ public function toArray(): array
+ {
+ return [
+ 'id' => $this->id,
+ 'title' => $this->title,
+ 'description' => $this->description,
+ 'start' => $this->start,
+ 'end' => $this->end,
+ 'allDay' => $this->allDay,
+ 'color' => $this->color,
+ ];
+ }
+}
diff --git a/src/Model/Menu.php b/src/Model/Menu.php
new file mode 100644
index 0000000..6b08950
--- /dev/null
+++ b/src/Model/Menu.php
@@ -0,0 +1,44 @@
+children = new ArrayCollection();
+
+ $bulk_data = $entityManager
+ ->createQuery('SELECT n FROM App\Entity\Page n WHERE n.parent IS null') // :Doctrine\ORM\Query
+ ->getResult(); // :array de Page
+
+ foreach($bulk_data as $first_level_entries){
+ // dans le menu
+ if($first_level_entries->isInMenu()){
+ $this->addChild($first_level_entries);
+ }
+ // autres pages
+ else{
+ // attention, seul le premier élément du chemin est pris en compte
+ $this->other_pages[] = $first_level_entries;
+ }
+ }
+
+ foreach($this->getChildren() as $page){
+ $page->fillChildrenPagePath();
+ }
+ }
+
+ public function getOtherPages(): array
+ {
+ return $this->other_pages;
+ }
+}
\ No newline at end of file
diff --git a/src/Model/Model.php b/src/Model/Model.php
new file mode 100644
index 0000000..fa8f764
--- /dev/null
+++ b/src/Model/Model.php
@@ -0,0 +1,316 @@
+entityManager = $entityManager;
+ $this->node = new Node; // instance mère "vide" ne possédant rien d'autre que des enfants
+ }
+
+ // à déplacer dans Path ou un truc comme ça?
+ // couper Model en deux classes NodeModel et PageModel?
+ public function makeMenuAndPaths(): void // lit la table "page"
+ {
+ self::$menu = new Menu($this->entityManager);
+ self::$page_path = new Path();
+ $this->page = self::$page_path->getLast();
+ }
+
+ public function getNode(): Node
+ {
+ return $this->node;
+ }
+ public function getArticleNode(): Node
+ {
+ return $this->article;
+ }
+
+ // affichage d'une page ordinaire
+ public function getWholePageData(Request $request): void // lit la table "node" + jointures
+ {
+ $id = CURRENT_PAGE === 'article' ? htmlspecialchars($request->query->get('id')) : '';
+
+ if($id === ''){ // page "normale"
+ // récupérer tous les noeuds sauf les articles
+ $dql = "SELECT n FROM App\Entity\Node n WHERE n.name_node != 'new' AND n.name_node != 'post' AND (n.page = :page OR n.page IS null)";
+ $bulk_data = $this->entityManager
+ ->createQuery($dql)
+ ->setParameter('page', $this->page)
+ ->getResult();
+
+ foreach($bulk_data as $parent_block){
+ // groupes d'articles triés par bloc, permet de paginer par bloc
+ if(Presentation::hasPresentation($parent_block->getName())){ // = post_block ou news_block
+ $bulk_data = array_merge($bulk_data, $this->getNextArticles($parent_block, $request)[0]);
+ }
+
+ // emails
+ if($parent_block->getName() === 'show_emails'){ // show_emails = l'afficheur
+ $parent_block->getNodeData()->setEmails($this->getAllEmails());
+
+ // aller chercher les formulaires à la place (future MAJ?)
+ //$this->getFormsNodeData();
+ }
+ }
+ }
+ else{ // page "article"
+ $dql = 'SELECT n FROM App\Entity\Node n WHERE n.page = :page OR n.page IS null OR n.id_node = :id';
+ $bulk_data = $this->entityManager
+ ->createQuery($dql)
+ ->setParameter('page', $this->page)
+ ->setParameter('id', $id)
+ ->getResult();
+ }
+
+ $this->makeNodeTree($bulk_data);
+ }
+
+ // récupération d'articles
+ public function getNextArticles(Node $parent_block, Request $request): array
+ {
+ $qb = $this->entityManager->createQueryBuilder();
+ $qb->select('n')
+ ->from('App\Entity\Node', 'n')
+ ->where('n.parent = :parent')
+ ->setParameter('parent', $parent_block);
+
+ if($parent_block->getName() === 'post_block'){
+ $qb->orderBy('n.position');
+ }
+ elseif($parent_block->getName() === 'news_block'){
+ $qb->join('n.article', 'a');
+ if($parent_block->getNodeData()->getChronoOrder() ?? false){ // ordre antichrono par défaut
+ $qb->orderBy('a.date_time', 'ASC');
+ }
+ else{
+ $qb->orderBy('a.date_time', 'DESC');
+ }
+ }
+
+ // pagination
+ $limit = $parent_block->getNodeData()->getPaginationLimit(); // = 12 par défaut si = null en BDD
+ $this->paginateWithCursor($qb, $parent_block, $request->query->get('last_article'));
+ $result = $qb->getQuery()->getResult();
+
+ // il reste des articles à récupérer SI on vient d'en récupérer trop
+ // ET on gère le cas particulier de $limit <= 0
+ $truncated = false;
+ if(count($result) > $limit && $limit > 0){ // si nb résultat > limit > 0
+ $truncated = true;
+ array_pop($result); // compenser le $limit + 1 dans paginateWithCursor
+ }
+
+ return [$result, $truncated]; // besoin exceptionnel de retourner deux valeurs
+ }
+
+ private function paginateWithCursor(QueryBuilder $qb, Node $parent_block, ?string $last_article): void
+ {
+ //var_dump($last_article);
+ $limit = $parent_block->getNodeData()->getPaginationLimit(); // = 12 par défaut si = null en BDD
+
+ if($limit > 0){ // si 0 ou moins pas de pagination
+ // nombres de "pages" d'articles
+ $nb_pages = $this->getNumberOfPages($parent_block, $limit);
+ $parent_block->getNodeData()->setNumberOfPages($nb_pages > 1 ? $nb_pages : 1);
+
+ // adaptation de la requête
+ if($parent_block->getName() === 'post_block'){
+ $qb->andWhere('n.position > :last_position')
+ ->setParameter('last_position', empty($last_article) ? 0 : $last_article)
+ ->setMaxResults($limit + 1);
+ }
+ elseif($parent_block->getName() === 'news_block'){
+ $cursor_start = $parent_block->getNodeData()->getChronoOrder() ? '1970-01-01' : '9999-12-31';
+ $qb->andWhere($parent_block->getNodeData()->getChronoOrder() ? 'a.date_time > :date_time' : 'a.date_time < :date_time')
+ ->setParameter('date_time', empty($last_article) ? $cursor_start : $last_article)
+ ->setMaxResults($limit + 1);
+ }
+ }
+ }
+
+ // le Paginator de doctrine le fait aussi si on décidait de s'en servir
+ private function getNumberOfPages(Node $parent_block, int $limit): int
+ {
+ $dql = 'SELECT COUNT(n.id_node) FROM App\Entity\Node n WHERE n.parent = :parent';
+ $nb_articles = $this->entityManager
+ ->createQuery($dql)
+ ->setParameter('parent', $parent_block)
+ ->getSingleScalarResult();
+ return (int)ceil($nb_articles / $limit); // que PHP fasse une division non euclidienne (pas comme en C) nous arrange ici
+ }
+
+ private function makeNodeTree(array $bulk_data): void // $bulk_data = tableau de Node
+ {
+ // puis on les range
+ // (attention, risque de disfonctionnement si les noeuds de 1er niveau ne sont pas récupérés en 1er dans la BDD)
+ foreach($bulk_data as $node){
+ // premier niveau
+ if($node->getParent() == null){
+ $this->node->addChild($node);
+
+ // spécifique page article
+ if($node->getName() === 'main' && $this->page->getEndOfPath() == 'article'){
+ $main = $node;
+ }
+ }
+ // autres niveaux
+ else{
+ $node->getParent()->addChild($node);
+
+ // spécifique page article
+ if($this->page->getEndOfPath() == 'article'){
+ if($node->getName() === 'new'){
+ $new = $node;
+ }
+ }
+ }
+ }
+ if(isset($new)){
+ $main->setAdoptedChild($new);
+ }
+ }
+
+ // le basique
+ public function findNodeById(int $id): bool
+ {
+ $this->node = $this->entityManager->find('App\Entity\Node', $id);
+ return $this->node === null ? false : true;
+ }
+ public function findWhateverNode(string $field, string $value): bool
+ {
+ $queryBuilder = $this->entityManager->createQueryBuilder();
+ $queryBuilder
+ ->select('n')
+ ->from('App\Entity\Node', 'n')
+ ->where("n.$field = :value") // avec le querybuilder, ce truc sale reste sécurisé
+ ->setParameter('value', $value);
+ $result = $queryBuilder->getQuery()->getOneOrNullResult();
+
+ if($result === null){
+ return false;
+ }
+ else{
+ $this->node = $result;
+ return true;
+ }
+ }
+ public function getWhatever(string $class, string $field, string $value): array
+ {
+ // penser au entityManager "repository"
+ $queryBuilder = $this->entityManager->createQueryBuilder();
+ $queryBuilder
+ ->select('n')
+ ->from($class, 'n')
+ ->where("n.$field = :value")
+ ->setParameter('value', $value);
+ return $queryBuilder->getQuery()->getResult();
+ }
+
+ // récupération d'un article pour modification
+ public function makeArticleNode(string $id = '', bool $get_section = false): bool
+ {
+ if($get_section){
+ $dql = 'SELECT n, p FROM App\Entity\Node n LEFT JOIN n.parent p WHERE n.id_node = :id';
+ }
+ else{
+ $dql = 'SELECT n FROM App\Entity\Node n WHERE n.id_node = :id';
+ }
+ // n est l'article et p son $parent
+ $bulk_data = $this->entityManager
+ ->createQuery($dql)
+ ->setParameter('id', $id)
+ ->getResult();
+
+ if(count($bulk_data) === 0){
+ return false;
+ }
+
+ if($get_section){
+ $this->article = $bulk_data[0];
+ $this->findNodeById($bulk_data[0]->getParent()->getId());
+ $this->makeSectionNode();
+ }
+ else{
+ $this->article = $bulk_data[0];
+ }
+
+ return true;
+ }
+
+ // récupération des articles d'un bloc
Accès aux backups impossible faute de permissions.");
+ }
+
+ $backup_array = [];
+ foreach($files as $file){
+ if($file[0] === '.'){
+ continue;
+ }
+ $backup_array[] = $file;
+ }
+ return $backup_array;
+ }
+
+ static public function cleanBackups(): void
+ {
+ $files = glob(self::$backup_dir . '/*.sql');
+ //usort($files, fn($a, $b) => filemtime($b) <=> filemtime($a)); // filemtime = date de dernière modification
+ arsort($files);
+
+ // tri par nom de BDD puis par date
+ $sorted_files = [];
+ $list_by_database = []; // pour le nettoyage 2
+ foreach($files as $file){
+ $exploded = explode('_', basename($file));
+ $sorted_files[$exploded[0]][$exploded[1]][] = $file;
+ $list_by_database[$exploded[0]][] = $file;
+ }
+
+ $date = new \DateTime;
+ foreach($sorted_files as $db_name => $from_one_database){
+ // on garde une "quantité à garder" par BDD
+ if(count($from_one_database) > self::$amount_to_keep){
+ // nettoyage 1
+ foreach($from_one_database as $date_key => $with_same_date){
+ // pas touche à aujourd'hui
+ if($date_key != $date->format('Y-m-d')){
+ self::cleanBackupsByPriority($with_same_date);
+ }
+ }
+ // nettoyage 2
+ $files_to_delete = array_slice($list_by_database[$db_name], self::$amount_to_keep);
+ foreach($files_to_delete as $file){
+ unlink($file);
+ }
+ }
+ }
+ }
+
+ // conserver un seul backup par jour choisi dans cet ordre de préférence: console => before-restore => download => auto
+ // cet ordre correspond à: volonté de l'utilisateur => état du jour avant changement => volonté de changement => automatique sans contrôle
+ static private function cleanBackupsByPriority(array $files): void
+ {
+ $priorities = [
+ 'console' => 1,
+ 'before-restore' => 2,
+ 'uploaded' => 3,
+ 'auto' => 4,
+ ];
+ $best_priority = PHP_INT_MAX;
+
+ // recherche du fichier à conserver
+ $to_keep = null;
+ foreach($files as $file){
+ foreach($priorities as $keyword => $priority){
+ if(str_contains(basename($file), $keyword) && $priority < $best_priority){
+ $best_priority = $priority;
+ $to_keep = $file;
+ break;
+ }
+ }
+ }
+ // suppression des autres
+ foreach($files as $file){
+ if($file !== $to_keep){
+ unlink($file);
+ }
+ }
+ }
+
+ static public function restoreDatabase(EntityManager $entityManager, string $file_name, array $exclusion_list = []): void
+ {
+ // création d'un backup de sécurité non écrasable
+ $date = new \DateTime;
+ if(!file_exists(self::$backup_dir . '/' . Config::$database . '_' . $date->format('Y-m-d') . '_before-restore.sql')){
+ Backup::mySQLdump($entityManager, 'before-restore');
+ }
+
+ $version = $entityManager->getConnection()->fetchOne('SELECT VERSION()');
+ $engine = stripos($version, 'mariadb') !== false ? 'mariadb' : 'mysql';
+
+ $tables = self::getDBListOfTables($entityManager, $exclusion_list); // choisir les tables à effacer et restaurer, exclure "user"
+
+ $tmp_db_codes = tempnam('../var', 'tmp_db_codes_'); // crée un fichier avec un nom aléatoire et des droits 600 (concurrence)
+ file_put_contents($tmp_db_codes,
+ "[client]\n
+ user=" . Config::$user . "\n
+ password=" . Config::$password . "\n
+ host=" . Config::$db_host . "\n");
+
+ //$file_name = self::gzipExtract($file_name); // '.gz' ajouté à la fin
+
+ $command = new Process([
+ $engine, // mariadb ou mysql
+ '--defaults-extra-file=' . $tmp_db_codes, // pour ne pas enregistrer les codes dans l'historique de la console ou dans les processus de l'OS
+ Config::$database
+ ]);
+ $command->setInput(file_get_contents(Backup::$backup_dir . '/' . $file_name)); // l'entrée <
+
+ // utiliser une liste de tables à exclure dynamique
+ try{
+ // tout effacer
+ $tables_with_backquotes = array_map(fn($t) => '`' . $t . '`', $tables);
+ if(!empty($tables_with_backquotes)){
+ $sql = "SET FOREIGN_KEY_CHECKS=0; DROP TABLE " . implode(', ', $tables_with_backquotes) . "; SET FOREIGN_KEY_CHECKS=1;";
+ $entityManager->getConnection()->executeStatement($sql);
+ }
+
+ // copie des tables à ne pas restaurer
+ foreach($exclusion_list as $excluded){
+ $entityManager->getConnection()->executeStatement('RENAME TABLE `' . TABLE_PREFIX . $excluded . '` TO `' . TABLE_PREFIX . $excluded . '_dont_touch`;');
+ }
+
+ // restaurer
+ $command->mustRun(); // comme run() mais lance une ProcessFailedException
+
+ // remettre table user comme avant
+ foreach($exclusion_list as $excluded){
+ $entityManager->getConnection()->executeStatement('DROP TABLE `' . TABLE_PREFIX . $excluded . '`;');
+ $entityManager->getConnection()->executeStatement('RENAME TABLE `' . TABLE_PREFIX . $excluded . '_dont_touch` TO `' . TABLE_PREFIX . $excluded . '`;');
+ }
+ }
+ finally{
+ // exécuté même quand situé après "return"
+ unlink($tmp_db_codes);
+ }
+ }
+
+ static private function getDBListOfTables(EntityManager $entityManager, array $exclusion_list = []): array
+ {
+ $tables = $entityManager->getConnection()->createSchemaManager()->listTableNames();
+ foreach($tables as $key => $elem){
+ foreach($exclusion_list as $excluded){
+ if(!is_string($excluded)){ // cas pas du tout censé arriver!
+ throw new \LogicException("Un nom de table doit etre une chaîne de caractères.");
+ }
+ if($elem === TABLE_PREFIX . $excluded){
+ unset($tables[$key]);
+ break; // petite optimisation
+ }
+ }
+ }
+ return $tables;
+ }
+
+ static public function createZip(string $zip_name, array $source_directories, array $pattern_to_target_in_user_data = []): string
+ {
+ $file_path = '../var/' . $zip_name;
+ $Zip = new \ZipArchive();
+
+ if($Zip->open($file_path, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== TRUE){ // ou ZipArchive::FL_OVERWRITE, à comparer à ZipArchive::OVERWRITE
+ throw new \RuntimeException("Création ou ouverture du fichier demandé impossible à cause d'une erreur de permissions sur le serveur.");
+ }
+
+ // recherche récursive dans les dossiers dans $source_directories, c'est comme le paramètre "-r" dans la console
+ $counter = 0;
+ foreach($source_directories as $path){
+ $directory = new \RecursiveDirectoryIterator($path);
+ $iterator = new \RecursiveIteratorIterator($directory);
+
+ foreach($iterator as $info){
+ if($info->getFilename() != "." && $info->getFilename() != ".." && $info->getFilename() != '.htaccess'){ // chemins inutiles . et .. et le .htaccess
+ $Zip->addGlob($info->getPathname(), 0, array(''));
+ $counter++;
+ }
+ }
+ }
+ // recherche à la racine avec des pattern de noms de fichiers (optionnel)
+ foreach($pattern_to_target_in_user_data as $one_pattern){
+ $Zip->addGlob($one_pattern, 0, array(''));
+ }
+
+ $Zip->close();
+ if($counter > 0){
+ //chmod($file_path, 0666);
+ return $zip_name;
+ }
+ else{
+ throw new \RuntimeException("Téléchargement des fichiers impossible, aucun fichier n'a été trouvé sur le serveur.");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Service/Captcha.php b/src/Service/Captcha.php
new file mode 100644
index 0000000..28c7f71
--- /dev/null
+++ b/src/Service/Captcha.php
@@ -0,0 +1,57 @@
+ on pourrait appliquer le pattern "singleton" (justification = le captcha devient une sorte de ressource partagée)
+
+declare(strict_types=1);
+
+namespace App\Service;
+
+class Captcha
+{
+ private int $a;
+ private int $b;
+
+ public function __construct(){
+ $this->a = rand(2, 9);
+ $this->b = rand(2, 9);
+ }
+
+ public function getA(): string
+ {
+ return $this->toLettersFrench($this->a);
+ }
+ public function getB(): string
+ {
+ return $this->toLettersFrench($this->b);
+ }
+ public function getSolution(): int
+ {
+ return ($this->a * $this->b);
+ }
+
+ private function toLettersFrench(int $number): string
+ {
+ return match($number){
+ 2 => 'deux',
+ 3 => 'trois',
+ 4 => 'quatre',
+ 5 => 'cinq',
+ 6 => 'six',
+ 7 => 'sept',
+ 8 => 'huit',
+ 9 => 'neuf',
+ default => '', // erreur
+ };
+ }
+
+ // (à déplacer dans FormValidation?)
+ static public function controlInput(string $input = '0'): int
+ {
+ // un POST est une chaîne qu'on doit convertir en nombre dans deux conditions:
+ // test de format: $input est un nombre
+ // test d'intégrité: supprimer les décimales avec (int) ne change pas la valeur du nombre
+ return is_numeric($input) && $input == (int)$input ? (int)$input : 0;
+ }
+}
\ No newline at end of file
diff --git a/src/Service/Config.php b/src/Service/Config.php
new file mode 100644
index 0000000..fc4d127
--- /dev/null
+++ b/src/Service/Config.php
@@ -0,0 +1,90 @@
+Le fichier config/config.ini n'existe pas ou n'est pas lisible.
L'extension dom est manquante.
"); + $flag = true; + } + + /*if(!extension_loaded('imagick') && !extension_loaded('gd')){ + echo("il manque une de ces extensions au choix pour le traitement des images: imagick (de préférence) ou gd.
"); + $flag = true; + }*/ + // si imagick n'est pas disponible, essayer gd (reste encore à coder) + + if($flag){ + echo 'Réalisez les actions nécéssaires sur le serveur ou contactez l\'administrateur du site.
+ Quand le problème sera résolu, il vous suffira de recharger la page.
Le dossier public/user_data introuvable et le serveur n\'a pas la permission de le créer.
+ Pour faire ça bien:
sudo -u "serveur web" mkdir /chemin/du/site/public/user_data
Le fichier config/config.ini est introuvable.
'; + echo 'Il doit obligatoirement contenir les codes de la base de données, le protocole http ou https (et éventuellement le port) utilisé pour créer les liens internes.
+ Un modèle est disponible, il s\'agit du fichier config/config-template.ini
Ce fichier a une importance critique. Si vous le pouvez faites en sorte que le serveur en soit le propriétaire et donner lui des droits 600.
'; + $flag = true; + } + /*else{ + // propriétaire du fichier + if(fileowner('../config/config.ini') != posix_geteuid()){ + echo "le fichier config/config.ini n'appartient pas au serveur.
"; + } + else{ + // droits du config.ini + if(substr(sprintf('%o', fileperms('../config/config.ini')), -4) != 600){ + echo 'Attention, le
'; + //chmod('../config/config.ini', $droits_fichiers); + } + } + }*/ + if($flag){ + die; + } + + // tester les liens internes + // + + // le test de connexion à la BDD est dans le doctrine bootstrap + } +} \ No newline at end of file diff --git a/src/Service/Router.php b/src/Service/Router.php new file mode 100644 index 0000000..98cecc5 --- /dev/null +++ b/src/Service/Router.php @@ -0,0 +1,393 @@ + 1er test, méthode http GET? POST? +=> 2ème test, type de contenu: +"application/x-www-form-urlencoded" = formulaire +"application/json" = requête AJAX avec fetch() +"multipart/form-data" = upload d'image par tinymce +$_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest' requête AJAX xhs, non utilisée +=> 3ème test, comme le 2ème test mais uniquement si IS_ADMIN est vrai */ + +/* classes de réponses pour les contrôleurs +Response($html, Response::HTTP_OK) page html +JsonResponse(['success' => true, 'data' => $data]) ajax +RedirectResponse('index.php?page=login') redirection +BinaryFileResponse($filePath) téléchargement +StreamedResponse(function () {echo "ligne 1\n";echo "ligne 2\n";}) gros fichier */ + +// relire ça à l'occaz: +// https://symfony.com/doc/current/introduction/from_flat_php_to_symfony.html + +declare(strict_types=1); + +namespace App\Service; + +use Symfony\Component\HttpFoundation\Request; +use Doctrine\ORM\EntityManager; +use Symfony\Component\HttpFoundation\JsonResponse; +use Symfony\Component\HttpFoundation\RedirectResponse; +use Symfony\Component\HttpFoundation\Response; +use App\Service\User; +use App\View\AbstractBuilder; +use App\Model\Model; +use App\Model\Menu; +use App\Controller\UserController; +use App\Controller\ArticleController; +use App\Controller\CalendarController; +use App\Controller\MaintenanceController; +use App\Controller\ViewController; +use App\Controller\ContactFormController; +use App\Controller\ImageUploadController; +use App\Controller\HeadFootController; +use App\Controller\MenuAndPathsController; +use App\Controller\PageManagementController; +use App\Controller\FileUploadController; + +class Router{ + private Request $request; + private EntityManager $entityManager; + private string $route = ''; // défaut page d'accueil + + public function __construct(Request $request, EntityManager $entityManager){ + $this->request = $request; + $this->entityManager = $entityManager; + + if(!User::existUsers($entityManager)){ // table "user" vide + $this->route = 'no_user'; + } + } + + public function dispatch(): Response + { + if($this->request->getMethod() === 'GET'){ + // table "user" vide + if($this->route === 'no_user'){ + ob_start(); + require AbstractBuilder::VIEWS_PATH . 'user_create.php'; + return new Response(ob_get_clean()); + } + + // bouton déconnexion (méthode GET parce que l'utilisateur ne modifie plus de données à partir de là) + if($this->request->query->get('action') === 'deconnection'){ + return UserController::disconnect(); // retourne un RedirectResponse + } + + // articles suivants + if($this->request->query->get('fetch') === 'next_articles'){ + return ArticleController::fetch($this->entityManager, $this->request); // retourne un JsonResponse + } + + // données du calendrier + // création du calendrier ou changement de dates affichées (boutons flèches mais pas changement de vue) + if($this->request->query->get('action') === 'get_events' + && $this->request->query->has('start') && $this->request->query->has('end') && empty($this->request->getPayload()->all())){ // getPayload ne récupère pas que des POST + return CalendarController::getData($this->entityManager); + } + + // pages interdites + if(!IS_ADMIN && in_array(CURRENT_PAGE, ['menu_paths', 'new_page', 'user_edit', 'emails', 'maintenance'])){ + return new RedirectResponse((string)new URL); + } + + if(IS_ADMIN){ + if($this->request->query->get('action') === 'get_mysqldump'){ + return MaintenanceController::getLastDump($this->entityManager); + } + if($this->request->query->get('action') === 'get_all_media'){ + return MaintenanceController::getAllMedia(); + } + } + + // affichage d'une page + return ViewController::getWebPage($this->entityManager, $this->request); + } + + + elseif($this->request->getMethod() === 'POST'){ + /* -- contrôleurs appelables par tout le monde -- */ + + // table "user" vide + if($this->route === 'no_user'){ + return UserController::createAdminUser($this->entityManager); + } + + // requête JSON avec fetch() + if($this->request->headers->get('Content-Type') === 'application/json') + { + $json = json_decode($this->request->getContent(), true); // = json_decode(file_get_contents('php://input'), true); + + // formulaire de contact + if($this->request->query->get('action') === 'send_email'){ + return ContactFormController::sendVisitorEmail($this->entityManager, $json); + } + /*else{ + return new JsonResponse(['success' => false, 'error' => 'tu fais quoi là mec?'], JsonResponse::HTTP_BAD_REQUEST); + }*/ + } + + // envoi formulaire HTML + elseif($this->request->headers->get('Content-Type') === 'application/x-www-form-urlencoded'){ + // tentative de connexion + if($this->request->query->get('action') === 'connection'){ + return UserController::connect($this->entityManager); + } + /*else{ + return new RedirectResponse((string)new URL(['error' => 'tu fais quoi là mec?'])); + }*/ + } + + if(IS_ADMIN){ + /* -- requêtes AJAX -- */ + + // requêtes JSON avec fetch() + if($this->request->headers->get('Content-Type') === 'application/json'){ + $json = json_decode($this->request->getContent(), true); // = json_decode(file_get_contents('php://input'), true); + + if($this->request->query->has('action')){ + /* -- manipulation des articles -- */ + if($this->request->query->get('action') === 'editor_submit' && isset($json['id']) && isset($json['content'])){ + return ArticleController::editorSubmit($this->entityManager, $json); + } + elseif($this->request->query->get('action') === 'delete_article' && isset($json['id'])){ + return ArticleController::deleteArticle($this->entityManager, $this->request); // version AJAX + } + elseif($this->request->query->get('action') === 'switch_positions' && isset($json['id1']) && isset($json['id2'])){ + return ArticleController::switchPositions($this->entityManager, $json); + } + elseif($this->request->query->get('action') === 'date_submit' && isset($json['id']) && isset($json['date'])){ + return ArticleController::dateSubmit($this->entityManager, $json); + } + + switch($this->request->query->get('action')){ + /* -- bloc Formulaire -- */ + case 'keep_emails': + return ContactFormController::keepEmails($this->entityManager, $json); + case 'set_retention_period': + return ContactFormController::setEmailsRetentionPeriod($this->entityManager, $json); + case 'set_email_param': + return ContactFormController::setEmailParam($this->entityManager, $json); + case 'test_email': + return ContactFormController::sendTestEmail($this->entityManager, $json); + + /* -- page emails -- */ + case 'delete_email': + return ContactFormController::deleteEmail($this->entityManager, $json); + case 'toggle_sensitive_email': + return ContactFormController::toggleSensitiveEmail($this->entityManager, $json); + + /* -- upload d'image dans tinymce par copier-coller -- */ + // collage de HTML contenant une ou plusieurs balises