From f27cca08ff786ceff1870147f6d6bad14d71117f Mon Sep 17 00:00:00 2001 From: polo Date: Fri, 23 Oct 2026 00:47:32 +0200 Subject: =?UTF-8?q?dossiers=20avec=20majuscule,=20namespace=20pour=20toute?= =?UTF-8?q?s=20les=20classes,=20autoload=20psr-4,=20UserDataService::creat?= =?UTF-8?q?eZip=20d=C3=A9plac=C3=A9=20dans=20Backup?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- bin/copy_directory.php | 6 +- bin/cron.php | 7 +- bin/doctrine | 7 +- bin/mysqldump.php | 5 +- bin/restore_db.php | 5 +- composer.json | 7 +- public/index.php | 10 +- src/Controller/ArticleController.php | 244 +++++++++++++++++ src/Controller/CalendarController.php | 77 ++++++ src/Controller/ContactFormController.php | 107 ++++++++ src/Controller/FileUploadController.php | 68 +++++ src/Controller/HeadFootController.php | 146 +++++++++++ src/Controller/ImageUploadController.php | 194 ++++++++++++++ src/Controller/MaintenanceController.php | 152 +++++++++++ src/Controller/MenuAndPathsController.php | 226 ++++++++++++++++ src/Controller/PageManagementController.php | 368 ++++++++++++++++++++++++++ src/Controller/UserController.php | 237 +++++++++++++++++ src/Controller/ViewController.php | 70 +++++ src/Model/DatabaseSettingUp.php | 157 +++++++++++ src/Model/Entity/AppMetadata.php | 46 ++++ src/Model/Entity/Article.php | 94 +++++++ src/Model/Entity/Asset.php | 68 +++++ src/Model/Entity/AssetEmployment.php | 54 ++++ src/Model/Entity/Email.php | 131 ++++++++++ src/Model/Entity/EmailForm.php | 57 ++++ src/Model/Entity/Event.php | 129 +++++++++ src/Model/Entity/Image.php | 89 +++++++ src/Model/Entity/Log.php | 38 +++ src/Model/Entity/Node.php | 167 ++++++++++++ src/Model/Entity/NodeData.php | 102 ++++++++ src/Model/Entity/Page.php | 222 ++++++++++++++++ src/Model/Entity/Presentation.php | 135 ++++++++++ src/Model/Entity/User.php | 67 +++++ src/Model/EventDTO.php | 53 ++++ src/Model/Menu.php | 44 ++++ src/Model/Model.php | 316 ++++++++++++++++++++++ src/Model/Path.php | 80 ++++++ src/Model/Position.php | 52 ++++ src/Model/doctrine-bootstrap.php | 34 +++ src/Service/Backup.php | 272 +++++++++++++++++++ src/Service/Captcha.php | 57 ++++ src/Service/Config.php | 90 +++++++ src/Service/EmailService.php | 106 ++++++++ src/Service/FormValidation.php | 216 +++++++++++++++ src/Service/Installation.php | 109 ++++++++ src/Service/Router.php | 393 ++++++++++++++++++++++++++++ src/Service/Security.php | 112 ++++++++ src/Service/URL.php | 90 +++++++ src/Service/User.php | 25 ++ src/Service/session.php | 94 +++++++ src/View/AbstractBuilder.php | 81 ++++++ src/View/BreadcrumbBuilder.php | 53 ++++ src/View/CalendarBuilder.php | 31 +++ src/View/FooterBuilder.php | 122 +++++++++ src/View/FormBuilder.php | 55 ++++ src/View/GaleryBuilder.php | 72 +++++ src/View/HeadBuilder.php | 84 ++++++ src/View/HeaderBuilder.php | 131 ++++++++++ src/View/LoginBuilder.php | 56 ++++ src/View/MainBuilder.php | 99 +++++++ src/View/MaintenanceBuilder.php | 36 +++ src/View/MenuBuilder.php | 101 +++++++ src/View/NavBuilder.php | 77 ++++++ src/View/NewBuilder.php | 157 +++++++++++ src/View/NewPageBuilder.php | 53 ++++ src/View/NewsBlockBuilder.php | 76 ++++++ src/View/PostBlockBuilder.php | 90 +++++++ src/View/PostBuilder.php | 60 +++++ src/View/ShowEmailsBuilder.php | 71 +++++ src/View/UserEditBuilder.php | 64 +++++ src/View/ViewDirector.php | 30 +++ src/View/templates/calendar.php | 14 + src/View/templates/footer.php | 43 +++ src/View/templates/form.php | 36 +++ src/View/templates/form_admin.php | 73 ++++++ src/View/templates/galery.php | 12 + src/View/templates/head.php | 13 + src/View/templates/header.php | 58 ++++ src/View/templates/login.php | 31 +++ src/View/templates/maintenance.php | 65 +++++ src/View/templates/menu.php | 51 ++++ src/View/templates/modify_block.php | 61 +++++ src/View/templates/modify_page.php | 62 +++++ src/View/templates/new.php | 27 ++ src/View/templates/new_page.php | 38 +++ src/View/templates/news_block.php | 14 + src/View/templates/post.php | 14 + src/View/templates/post_block.php | 14 + src/View/templates/show_emails.php | 30 +++ src/View/templates/user_create.php | 54 ++++ src/View/templates/user_edit.php | 63 +++++ src/controller/ArticleController.php | 239 ----------------- src/controller/CalendarController.php | 74 ------ src/controller/ContactFormController.php | 102 -------- src/controller/FileUploadController.php | 65 ----- src/controller/HeadFootController.php | 141 ---------- src/controller/ImageUploadController.php | 191 -------------- src/controller/MaintenanceController.php | 147 ----------- src/controller/MenuAndPathsController.php | 219 ---------------- src/controller/PageManagementController.php | 362 ------------------------- src/controller/UserController.php | 230 ---------------- src/controller/ViewController.php | 63 ----- src/model/DatabaseSettingUp.php | 155 ----------- src/model/EventDTO.php | 51 ---- src/model/Menu.php | 42 --- src/model/Model.php | 312 ---------------------- src/model/Path.php | 76 ------ src/model/Position.php | 50 ---- src/model/doctrine-bootstrap.php | 33 --- src/model/entities/AppMetadata.php | 46 ---- src/model/entities/Article.php | 94 ------- src/model/entities/Asset.php | 68 ----- src/model/entities/AssetEmployment.php | 54 ---- src/model/entities/Email.php | 131 ---------- src/model/entities/EmailForm.php | 57 ---- src/model/entities/Event.php | 129 --------- src/model/entities/Image.php | 89 ------- src/model/entities/Log.php | 38 --- src/model/entities/Node.php | 166 ------------ src/model/entities/NodeData.php | 102 -------- src/model/entities/Page.php | 221 ---------------- src/model/entities/Presentation.php | 135 ---------- src/model/entities/User.php | 67 ----- src/service/Backup.php | 233 ----------------- src/service/Captcha.php | 55 ---- src/service/Config.php | 88 ------- src/service/EmailService.php | 104 -------- src/service/FormValidation.php | 214 --------------- src/service/Installation.php | 107 -------- src/service/Router.php | 376 -------------------------- src/service/Security.php | 110 -------- src/service/URL.php | 88 ------- src/service/User.php | 23 -- src/service/UserDataService.php | 46 ---- src/service/session.php | 90 ------- src/view/AbstractBuilder.php | 77 ------ src/view/BreadcrumbBuilder.php | 50 ---- src/view/CalendarBuilder.php | 29 -- src/view/FooterBuilder.php | 117 --------- src/view/FormBuilder.php | 50 ---- src/view/GaleryBuilder.php | 70 ----- src/view/HeadBuilder.php | 81 ------ src/view/HeaderBuilder.php | 129 --------- src/view/LoginBuilder.php | 52 ---- src/view/MainBuilder.php | 95 ------- src/view/MaintenanceBuilder.php | 33 --- src/view/MenuBuilder.php | 97 ------- src/view/NavBuilder.php | 73 ------ src/view/NewBuilder.php | 152 ----------- src/view/NewPageBuilder.php | 49 ---- src/view/NewsBlockBuilder.php | 73 ------ src/view/PostBlockBuilder.php | 88 ------- src/view/PostBuilder.php | 57 ---- src/view/ShowEmailsBuilder.php | 69 ----- src/view/UserEditBuilder.php | 62 ----- src/view/ViewDirector.php | 28 -- src/view/templates/calendar.php | 11 - src/view/templates/footer.php | 40 --- src/view/templates/form.php | 36 --- src/view/templates/form_admin.php | 72 ----- src/view/templates/galery.php | 12 - src/view/templates/head.php | 13 - src/view/templates/header.php | 55 ---- src/view/templates/login.php | 31 --- src/view/templates/maintenance.php | 62 ----- src/view/templates/menu.php | 48 ---- src/view/templates/modify_block.php | 58 ---- src/view/templates/modify_page.php | 58 ---- src/view/templates/new.php | 27 -- src/view/templates/new_page.php | 35 --- src/view/templates/news_block.php | 14 - src/view/templates/post.php | 14 - src/view/templates/post_block.php | 14 - src/view/templates/show_emails.php | 30 --- src/view/templates/user_create.php | 53 ---- src/view/templates/user_edit.php | 63 ----- 176 files changed, 7992 insertions(+), 7775 deletions(-) create mode 100644 src/Controller/ArticleController.php create mode 100644 src/Controller/CalendarController.php create mode 100644 src/Controller/ContactFormController.php create mode 100644 src/Controller/FileUploadController.php create mode 100644 src/Controller/HeadFootController.php create mode 100644 src/Controller/ImageUploadController.php create mode 100644 src/Controller/MaintenanceController.php create mode 100644 src/Controller/MenuAndPathsController.php create mode 100644 src/Controller/PageManagementController.php create mode 100644 src/Controller/UserController.php create mode 100644 src/Controller/ViewController.php create mode 100644 src/Model/DatabaseSettingUp.php create mode 100644 src/Model/Entity/AppMetadata.php create mode 100644 src/Model/Entity/Article.php create mode 100644 src/Model/Entity/Asset.php create mode 100644 src/Model/Entity/AssetEmployment.php create mode 100644 src/Model/Entity/Email.php create mode 100644 src/Model/Entity/EmailForm.php create mode 100644 src/Model/Entity/Event.php create mode 100644 src/Model/Entity/Image.php create mode 100644 src/Model/Entity/Log.php create mode 100644 src/Model/Entity/Node.php create mode 100644 src/Model/Entity/NodeData.php create mode 100644 src/Model/Entity/Page.php create mode 100644 src/Model/Entity/Presentation.php create mode 100644 src/Model/Entity/User.php create mode 100644 src/Model/EventDTO.php create mode 100644 src/Model/Menu.php create mode 100644 src/Model/Model.php create mode 100644 src/Model/Path.php create mode 100644 src/Model/Position.php create mode 100644 src/Model/doctrine-bootstrap.php create mode 100644 src/Service/Backup.php create mode 100644 src/Service/Captcha.php create mode 100644 src/Service/Config.php create mode 100644 src/Service/EmailService.php create mode 100644 src/Service/FormValidation.php create mode 100644 src/Service/Installation.php create mode 100644 src/Service/Router.php create mode 100644 src/Service/Security.php create mode 100644 src/Service/URL.php create mode 100644 src/Service/User.php create mode 100644 src/Service/session.php create mode 100644 src/View/AbstractBuilder.php create mode 100644 src/View/BreadcrumbBuilder.php create mode 100644 src/View/CalendarBuilder.php create mode 100644 src/View/FooterBuilder.php create mode 100644 src/View/FormBuilder.php create mode 100644 src/View/GaleryBuilder.php create mode 100644 src/View/HeadBuilder.php create mode 100644 src/View/HeaderBuilder.php create mode 100644 src/View/LoginBuilder.php create mode 100644 src/View/MainBuilder.php create mode 100644 src/View/MaintenanceBuilder.php create mode 100644 src/View/MenuBuilder.php create mode 100644 src/View/NavBuilder.php create mode 100644 src/View/NewBuilder.php create mode 100644 src/View/NewPageBuilder.php create mode 100644 src/View/NewsBlockBuilder.php create mode 100644 src/View/PostBlockBuilder.php create mode 100644 src/View/PostBuilder.php create mode 100644 src/View/ShowEmailsBuilder.php create mode 100644 src/View/UserEditBuilder.php create mode 100644 src/View/ViewDirector.php create mode 100644 src/View/templates/calendar.php create mode 100644 src/View/templates/footer.php create mode 100644 src/View/templates/form.php create mode 100644 src/View/templates/form_admin.php create mode 100644 src/View/templates/galery.php create mode 100644 src/View/templates/head.php create mode 100644 src/View/templates/header.php create mode 100644 src/View/templates/login.php create mode 100644 src/View/templates/maintenance.php create mode 100644 src/View/templates/menu.php create mode 100644 src/View/templates/modify_block.php create mode 100644 src/View/templates/modify_page.php create mode 100644 src/View/templates/new.php create mode 100644 src/View/templates/new_page.php create mode 100644 src/View/templates/news_block.php create mode 100644 src/View/templates/post.php create mode 100644 src/View/templates/post_block.php create mode 100644 src/View/templates/show_emails.php create mode 100644 src/View/templates/user_create.php create mode 100644 src/View/templates/user_edit.php delete mode 100644 src/controller/ArticleController.php delete mode 100644 src/controller/CalendarController.php delete mode 100644 src/controller/ContactFormController.php delete mode 100644 src/controller/FileUploadController.php delete mode 100644 src/controller/HeadFootController.php delete mode 100644 src/controller/ImageUploadController.php delete mode 100644 src/controller/MaintenanceController.php delete mode 100644 src/controller/MenuAndPathsController.php delete mode 100644 src/controller/PageManagementController.php delete mode 100644 src/controller/UserController.php delete mode 100644 src/controller/ViewController.php delete mode 100644 src/model/DatabaseSettingUp.php delete mode 100644 src/model/EventDTO.php delete mode 100644 src/model/Menu.php delete mode 100644 src/model/Model.php delete mode 100644 src/model/Path.php delete mode 100644 src/model/Position.php delete mode 100644 src/model/doctrine-bootstrap.php delete mode 100644 src/model/entities/AppMetadata.php delete mode 100644 src/model/entities/Article.php delete mode 100644 src/model/entities/Asset.php delete mode 100644 src/model/entities/AssetEmployment.php delete mode 100644 src/model/entities/Email.php delete mode 100644 src/model/entities/EmailForm.php delete mode 100644 src/model/entities/Event.php delete mode 100644 src/model/entities/Image.php delete mode 100644 src/model/entities/Log.php delete mode 100644 src/model/entities/Node.php delete mode 100644 src/model/entities/NodeData.php delete mode 100644 src/model/entities/Page.php delete mode 100644 src/model/entities/Presentation.php delete mode 100644 src/model/entities/User.php delete mode 100644 src/service/Backup.php delete mode 100644 src/service/Captcha.php delete mode 100644 src/service/Config.php delete mode 100644 src/service/EmailService.php delete mode 100644 src/service/FormValidation.php delete mode 100644 src/service/Installation.php delete mode 100644 src/service/Router.php delete mode 100644 src/service/Security.php delete mode 100644 src/service/URL.php delete mode 100644 src/service/User.php delete mode 100644 src/service/UserDataService.php delete mode 100644 src/service/session.php delete mode 100644 src/view/AbstractBuilder.php delete mode 100644 src/view/BreadcrumbBuilder.php delete mode 100644 src/view/CalendarBuilder.php delete mode 100644 src/view/FooterBuilder.php delete mode 100644 src/view/FormBuilder.php delete mode 100644 src/view/GaleryBuilder.php delete mode 100644 src/view/HeadBuilder.php delete mode 100644 src/view/HeaderBuilder.php delete mode 100644 src/view/LoginBuilder.php delete mode 100644 src/view/MainBuilder.php delete mode 100644 src/view/MaintenanceBuilder.php delete mode 100644 src/view/MenuBuilder.php delete mode 100644 src/view/NavBuilder.php delete mode 100644 src/view/NewBuilder.php delete mode 100644 src/view/NewPageBuilder.php delete mode 100644 src/view/NewsBlockBuilder.php delete mode 100644 src/view/PostBlockBuilder.php delete mode 100644 src/view/PostBuilder.php delete mode 100644 src/view/ShowEmailsBuilder.php delete mode 100644 src/view/UserEditBuilder.php delete mode 100644 src/view/ViewDirector.php delete mode 100644 src/view/templates/calendar.php delete mode 100644 src/view/templates/footer.php delete mode 100644 src/view/templates/form.php delete mode 100644 src/view/templates/form_admin.php delete mode 100644 src/view/templates/galery.php delete mode 100644 src/view/templates/head.php delete mode 100644 src/view/templates/header.php delete mode 100644 src/view/templates/login.php delete mode 100644 src/view/templates/maintenance.php delete mode 100644 src/view/templates/menu.php delete mode 100644 src/view/templates/modify_block.php delete mode 100644 src/view/templates/modify_page.php delete mode 100644 src/view/templates/new.php delete mode 100644 src/view/templates/new_page.php delete mode 100644 src/view/templates/news_block.php delete mode 100644 src/view/templates/post.php delete mode 100644 src/view/templates/post_block.php delete mode 100644 src/view/templates/show_emails.php delete mode 100644 src/view/templates/user_create.php delete mode 100644 src/view/templates/user_edit.php diff --git a/bin/copy_directory.php b/bin/copy_directory.php index 20e4821..0e0afe1 100644 --- a/bin/copy_directory.php +++ b/bin/copy_directory.php @@ -15,9 +15,9 @@ function copyDirectory(string $source, string $destination): void mkdir($destination, 0755, true); } - $iterator = new RecursiveIteratorIterator( - new RecursiveDirectoryIterator($source, RecursiveDirectoryIterator::SKIP_DOTS), - RecursiveIteratorIterator::SELF_FIRST + $iterator = new \RecursiveIteratorIterator( + new \RecursiveDirectoryIterator($source, \RecursiveDirectoryIterator::SKIP_DOTS), + \RecursiveIteratorIterator::SELF_FIRST ); foreach($iterator as $item){ diff --git a/bin/cron.php b/bin/cron.php index 5e158a4..14bb460 100644 --- a/bin/cron.php +++ b/bin/cron.php @@ -4,9 +4,12 @@ declare(strict_types=1); +use App\Service\Config; +use App\Service\EmailService; + chdir(dirname(__FILE__)); // = /chemin/du/site/bin = même niveau que public/ -require('../src/Config.php'); +require('../src/Service/Config.php'); Config::load('../config/config.ini'); -require '../src/model/doctrine-bootstrap.php'; +require '../src/Model/doctrine-bootstrap.php'; EmailService::cleanEmails($entityManager); \ No newline at end of file diff --git a/bin/doctrine b/bin/doctrine index 8cfd5d0..e92490c 100755 --- a/bin/doctrine +++ b/bin/doctrine @@ -4,14 +4,15 @@ use Doctrine\ORM\Tools\Console\ConsoleRunner; use Doctrine\ORM\Tools\Console\EntityManagerProvider\SingleManagerProvider; +use App\Service\Config; chdir(dirname(__FILE__)); // pour être au même niveau que l'appli dans /public -require('../src/service/Config.php'); +require('../src/Service/Config.php'); Config::load('../config/config.ini'); -require '../src/model/doctrine-bootstrap.php'; // relatif -//require __DIR__ . '/../src/model/doctrine-bootstrap.php'; // absolu +require '../src/Model/doctrine-bootstrap.php'; // relatif +//require __DIR__ . '/../src/Model/doctrine-bootstrap.php'; // absolu ConsoleRunner::run( new SingleManagerProvider($entityManager) diff --git a/bin/mysqldump.php b/bin/mysqldump.php index 7d8e960..3a63da9 100755 --- a/bin/mysqldump.php +++ b/bin/mysqldump.php @@ -4,10 +4,13 @@ declare(strict_types=1); +use App\Service\Config; +use App\Service\Backup; + chdir(dirname(__FILE__)); require "../vendor/autoload.php"; Config::load('../config/config.ini'); -require '../src/model/doctrine-bootstrap.php'; +require '../src/Model/doctrine-bootstrap.php'; $file_name = Backup::mySQLdump($entityManager, 'console'); // créer un nouveau backup echo "Un backup a été créé:\n" . realpath($file_name) . "\n"; \ No newline at end of file diff --git a/bin/restore_db.php b/bin/restore_db.php index c54aaa9..3fcddc1 100755 --- a/bin/restore_db.php +++ b/bin/restore_db.php @@ -4,6 +4,9 @@ declare(strict_types=1); +use App\Service\Config; +use App\Service\Backup; + if($argc === 2){ if(pathinfo($argv[1])['extension'] !== 'sql'){ try{ @@ -21,7 +24,7 @@ if($argc === 2){ chdir(dirname(__FILE__)); // /chemin/absolu/bin/restore_db.php require "../vendor/autoload.php"; Config::load('../config/config.ini'); -require '../src/model/doctrine-bootstrap.php'; +require '../src/Model/doctrine-bootstrap.php'; // backup sur le système de fichiers en paramètre if($argc === 2){ diff --git a/composer.json b/composer.json index 1978a0e..8c38f28 100644 --- a/composer.json +++ b/composer.json @@ -36,6 +36,9 @@ } ], "autoload": { - "classmap": ["src/"] - } + "psr-4": { + "App\\Entity\\": "src/Model/Entity/", + "App\\": "src/" + } + } } diff --git a/public/index.php b/public/index.php index 19011d1..6032dfd 100644 --- a/public/index.php +++ b/public/index.php @@ -14,7 +14,11 @@ declare(strict_types=1); use Symfony\Component\HttpFoundation\Request; - +use App\Model\DatabaseSettingUp; +use App\Service\Config; +use App\Service\URL; +use App\Service\Installation; +use App\Service\Router; /* -- partie 1: prétraitement -- code à exécuter pour toutes requêtes */ @@ -31,10 +35,10 @@ URL::setPort(Config::$port); URL::setHost($_SERVER['HTTP_HOST'] . Config::$index_path); // $entityManager -require '../src/model/doctrine-bootstrap.php'; // isDevMode est sur "true", DSN à adapter +require '../src/Model/doctrine-bootstrap.php'; // isDevMode est sur "true", DSN à adapter // session -require('../src/service/session.php'); +require('../src/Service/session.php'); startSession($entityManager); // tests de bon fonctionnement diff --git a/src/Controller/ArticleController.php b/src/Controller/ArticleController.php new file mode 100644 index 0000000..075e3b4 --- /dev/null +++ b/src/Controller/ArticleController.php @@ -0,0 +1,244 @@ +query->has('id') && !empty($request->query->get('id')) && $request->query->has('last_article')){ + $id = (int)$request->get('id'); // type et nettoie + $model = new Model($entityManager); + $model->findNodeById($id); + $parent_block = $model->getNode(); + + if(Presentation::hasPresentation($parent_block->getName())){ + $get_articles_return = $model->getNextArticles($parent_block, $request); + $bulk_data = $get_articles_return[0]; + + if($parent_block->getName() === 'post_block'){ + $builder_name = 'App\View\PostBuilder'; + } + elseif($parent_block->getName() === 'news_block'){ + $builder_name = 'App\View\NewBuilder'; + } + else{ + return new JsonResponse(['success' => false, 'error' => 'server side error'], JsonResponse::HTTP_INTERNAL_SERVER_ERROR); + } + + $html = ''; + foreach($bulk_data as $article){ + $builder = new $builder_name($article); + $html .= $builder->render(); + } + + return new JsonResponse(['success' => true, 'html' => $html, 'truncated' => $get_articles_return[1]]); + } + else{ + return new JsonResponse(['success' => false, 'error' => 'server side error'], JsonResponse::HTTP_INTERNAL_SERVER_ERROR); + } + } + else{ + return new JsonResponse(['success' => false, 'error' => 'bad parameters'], JsonResponse::HTTP_BAD_REQUEST); + } + } + + static public function editorSubmit(EntityManager $entityManager, array $json): JsonResponse + { + if(json_last_error() === JSON_ERROR_NONE){ + $id = $json['id']; + if(in_array($id[0], ['t', 'p', 'i', 'd'])){ + $id = substr($id, 1); + } + + $model = new Model($entityManager); + $content = $json['content']; + + // nettoyage + if(is_array($content)){ // cas d'une nouvelle "news" + foreach($content as $one_input){ + $one_input = Security::secureHTML($one_input); + } + } + else{ // autres cas + $content = Security::secureHTML($json['content']); + } + + // nouvel article + if($json['id'][0] === 'n'){ // ici $id est un bloc + $section_id = (int)substr($id, 1); // id du bloc
+ if(!$model->findNodeById($section_id)){ // erreur mauvais id + return new JsonResponse(['success' => false, 'error' => 'article_not_saved, bad id']); + } + $model->makeSectionNode(); + $section = $model->getNode(); + + // ajout d'une news + if(is_array($content)){ + if($section->getPage()->getEndOfPath() !== $json['from']){ // erreur mauvais from + return new JsonResponse(['success' => false, 'error' => 'article_not_saved, bad from']); + } + + $date = new \DateTime($content['d'] . ':' . (new \DateTime)->format('s')); // l'input type="datetime-local" ne donne pas les secondes, on les ajoute: 'hh:mm' . ':ss' + $article = new Article($content['i'], $date, $content['t'], $content['p']); + $article_node = new Node('new', count($section->getChildren()) + 1, $section, $section->getPage(), $article); + } + // ajout d'un post + else{ + $timestamp = time(); + $date = new \DateTime; + $date->setTimestamp($timestamp); + + $article = new Article($content, $date); + $placement = $json['placement'] === 'first' ? 0 : count($section->getChildren()) + 1; // + $article_node = new Node('post', $placement, $section, $section->getPage(), $article); + + if($json['placement'] === 'first'){ + $section->addChild($article_node); + $section->sortChildren(true); // mettre $article_node au début du tableau puis régénérer les positions (0 devient 1, 1 devient 2...) + } + } + + $entityManager->persist($article_node); + $entityManager->flush(); + + return new JsonResponse(['success' => true, 'article_id' => $article_node->getId()]); + } + // modification article + //else{} + + if($model->makeArticleNode($id)){ // une entrée est trouvée + $node = $model->getArticleNode(); + switch($json['id'][0]){ + case 'i': + $node->getArticle()->setContent($content); + break; + case 'p': + $node->getArticle()->setPreview($content); // html de l'éditeur + break; + case 't': + $node->getArticle()->setTitle($content); // html de l'éditeur + break; + case 'd': + return new JsonResponse(['success' => false, 'message' => 'l\'action editor_submit ne supporte pas les dates, utiliser date_submit.']); + default: // modif article simple (id sans lettre devant) + $node->getArticle()->setContent($content); + } + $entityManager->flush(); + return new JsonResponse(['success' => true]); + } + else{ + return new JsonResponse(['success' => false, 'message' => 'article non identifié']); + } + } + else{ + return new JsonResponse(['success' => false, 'message' => 'Erreur de décodage JSON']); + } + } + + static public function deleteArticle(EntityManager $entityManager, Request $request): Response + { + $model = new Model($entityManager); + + if($request->headers->get('Content-Type') === 'application/json'){ + $id = json_decode($request->getContent(), true)['id']; + } + elseif($request->headers->get('Content-Type') === 'application/x-www-form-urlencoded'){ + $id = $request->query->get('id'); + } + // ni JSON ni form, c'est quoi? un POST vide? + else{ + return new Response('la méthode deleteArticle ne peut être appelée de cette manière'); + } + + if(!$model->makeArticleNode($id, true)){ + $params = ['false', "Erreur 500 pas d\'article à supprimer"]; + } + else{ + $article = $model->getArticleNode(); + $section = $model->getNode(); + + $entityManager->remove($article); + $section->removeChild($article); + $section->sortChildren(true); // régénère les positions + + try{ + $entityManager->flush(); + $params = ['true', 'Article supprimé avec succès']; + } + catch(\Exception $e){ + $params = ['false', 'Erreur 500 ' . $e->getMessage()]; + } + } + + if($request->headers->get('Content-Type') === 'application/json'){ + return new JsonResponse( + ['success' => $params[0], 'message' => $params[1]], + $params[0] ? JsonResponse::HTTP_OK : JsonResponse::HTTP_INTERNAL_SERVER_ERROR + ); + } + elseif($request->headers->get('Content-Type') === 'application/x-www-form-urlencoded'){ + $url = new URL(['page' => $request->query->get('from') ?? '', 'success' => $params[0], 'message' => $params[1]]); + return new RedirectResponse((string)$url); + } + else{ + // cas inaccesible + throw new \Exception('la méthode deleteArticle ne peut être appelée de cette manière'); + } + } + + static public function switchPositions(EntityManager $entityManager, array $json): JsonResponse + { + $model = new Model($entityManager); + $model->makeArticleNode($json['id1'], true); + $article1 = $model->getArticleNode(); + $section = $model->getNode(); + + $section->sortChildren(true); // régénère les positions avant inversion + $article2 = null; + foreach($section->getChildren() as $child){ + if((string)$child->getId() === $json['id2']) // type string + { + $article2 = $child; + break; + } + } + + // inversion + $tmp = $article1->getPosition(); + $article1->setPosition($article2->getPosition()); + $article2->setPosition($tmp); + $entityManager->flush(); + + return new JsonResponse(['success' => true]); + } + + static public function dateSubmit(EntityManager $entityManager, array $json): JsonResponse + { + $id = substr($json['id'], 1); + $date = new \DateTime($json['date']); + + $model = new Model($entityManager); + $model->makeArticleNode($id); + $node = $model->getArticleNode(); + $node->getArticle()->setDateTime($date); + $entityManager->flush(); + + return new JsonResponse(['success' => true]); + } +} \ No newline at end of file diff --git a/src/Controller/CalendarController.php b/src/Controller/CalendarController.php new file mode 100644 index 0000000..679ff8a --- /dev/null +++ b/src/Controller/CalendarController.php @@ -0,0 +1,77 @@ +setTimezone(new \DateTimeZone('UTC')); + $end->setTimezone(new \DateTimeZone('UTC')); + + // affichage format ISO à l'heure UTC + //$date->format('Y-m-d\TH:i:s\Z'); + + // on prend les évènements se finissant après le début ou commençant avant la fin de la fourchette + $dql = 'SELECT e FROM App\Entity\Event e WHERE e.end >= :start AND e.start <= :end'; + $bulk_data = $entityManager->createQuery($dql) + ->setParameter('start', $start) + ->setParameter('end', $end) + ->getResult(); + + $events = []; + foreach($bulk_data as $one_entry){ + $event = new EventDTO($one_entry); + $events[] = $event->toArray(); + } + + return new JsonResponse($events); + } + + static public function newEvent(array $json, EntityManager $entityManager): JsonResponse + { + try{ + $event = new Event($json); + } + catch(\InvalidArgumentException $e){ + return new JsonResponse(['success' => false, 'error' => $e->getMessage()], JsonResponse::HTTP_BAD_REQUEST); // code 400 + } + $entityManager->persist($event); + $entityManager->flush(); + + return new JsonResponse(['success' => true, 'id' => $event->getId()]); + } + static public function updateEvent(array $json, EntityManager $entityManager): JsonResponse + { + $event = $entityManager->find('App\Entity\Event', (int)$json['id']); + try{ + $event->securedUpdateFromJSON($json); + } + catch(\InvalidArgumentException $e){ + return new JsonResponse(['success' => false, 'error' => $e->getMessage()], JsonResponse::HTTP_BAD_REQUEST); // code 400 + } + $entityManager->flush(); + + return new JsonResponse(['success' => true]); + } + static public function removeEvent(array $json, EntityManager $entityManager): JsonResponse + { + $event = $entityManager->find('App\Entity\Event', (int)$json['id']); + $entityManager->remove($event); + $entityManager->flush(); + + return new JsonResponse(['success' => true]); + } +} \ No newline at end of file diff --git a/src/Controller/ContactFormController.php b/src/Controller/ContactFormController.php new file mode 100644 index 0000000..56ef402 --- /dev/null +++ b/src/Controller/ContactFormController.php @@ -0,0 +1,107 @@ +find(EmailForm::class, $json['id']); + $form_data->updateData('keep_emails', $json['checked'] ? true : false); + $entityManager->persist($form_data); + $entityManager->flush(); + return new JsonResponse(['success' => true, 'checked' => $json['checked']]); + } + static public function setEmailsRetentionPeriod(EntityManager $entityManager, array $json): JsonResponse + { + $form_data = $entityManager->find(EmailForm::class, $json['id']); + $form_data->updateData($json['field'], (int)$json['months']); + $entityManager->persist($form_data); + $entityManager->flush(); + return new JsonResponse(['success' => true, 'months' => $json['months']]); + } + static public function setEmailParam(EntityManager $entityManager, array $json): JsonResponse + { + $form = new FormValidation($json, ValidationStrategy::EmailParam); + + if($form->validate()){ + $form_data = $entityManager->find(EmailForm::class, $json['id']); + $form_data->updateData($json['what_param'], trim($json['value'])); + $entityManager->persist($form_data); // ?? + $entityManager->flush(); + return new JsonResponse(['success' => true]); + } + else{ + return new JsonResponse(['success' => false, 'error' => $form->getError()]); + } + } + + // les deux méthodes suivantes sont "factorisables", elles ne se distinguent que par la gestion ou non du formulaire rempli par le visiteur + static public function sendVisitorEmail(EntityManager $entityManager, array $json): JsonResponse + { + $form = new FormValidation($json, ValidationStrategy::EmailSend); + + $error = ''; + if($form->validate()){ + // destinataire = e-mail par défaut dans config.ini OU choisi par l'utilisateur + $form_data = $entityManager->find(EmailForm::class, $json['id']); + if($form_data === null){ + return new JsonResponse(['success' => false, 'error' => 'server_error'], JsonResponse::HTTP_INTERNAL_SERVER_ERROR); // code 500 + } + + if(!EmailService::send($entityManager, $form_data, false, $form->getField('name'), $form->getField('email'), $form->getField('message'))){ + $error = 'email_not_sent'; + } + } + else{ + $error = $form->getError(); + } + + if(empty($error)){ + return new JsonResponse(['success' => true]); + } + else{ + return new JsonResponse(['success' => false, 'error' => $error]); + } + } + static public function sendTestEmail(EntityManager $entityManager, array $json): JsonResponse + { + // destinataire = e-mail par défaut dans config.ini OU choisi par l'utilisateur + $form_data = $entityManager->find(EmailForm::class, $json['id']); + if($form_data === null){ + return new JsonResponse(['success' => false, 'error' => 'server_error'], JsonResponse::HTTP_INTERNAL_SERVER_ERROR); + } + + if(EmailService::send($entityManager, $form_data, true, 'nom du visiteur', 'adresse@du_visiteur.fr', "TEST d'un envoi d'e-mail depuis le site web")){ + return new JsonResponse(['success' => true]); + } + else{ + return new JsonResponse(['success' => false, 'error' => 'email_not_sent']); + } + } + static public function deleteEmail(EntityManager $entityManager, array $json): JsonResponse + { + $email = $entityManager->find(Email::class, $json['id']); + $entityManager->remove($email); + $entityManager->flush(); + return new JsonResponse(['success' => true]); + } + static public function toggleSensitiveEmail(EntityManager $entityManager, array $json): JsonResponse + { + $email = $entityManager->find(Email::class, $json['id']); + $email->makeSensitive($json['checked']); + $entityManager->flush(); + return new JsonResponse(['success' => true, 'checked' => $json['checked'], 'deletion_date' => $email->getDeletionDate()->format('d/m/Y')]); + } +} \ No newline at end of file diff --git a/src/Controller/FileUploadController.php b/src/Controller/FileUploadController.php new file mode 100644 index 0000000..4627017 --- /dev/null +++ b/src/Controller/FileUploadController.php @@ -0,0 +1,68 @@ + $file_path]); + } + else{ + return new JsonResponse(['message' => 'Erreur enregistrement du fichier.'], JsonResponse::HTTP_INTERNAL_SERVER_ERROR); // code 500 + } + } + else{ + return new JsonResponse(['message' => 'Erreur 400: fichier non valide.'], JsonResponse::HTTP_BAD_REQUEST); // code 400 + } + } + else{ + return new JsonResponse(['message' => 'Erreur 400: Bad Request'], JsonResponse::HTTP_BAD_REQUEST); // code 400 + } + } +} \ No newline at end of file diff --git a/src/Controller/HeadFootController.php b/src/Controller/HeadFootController.php new file mode 100644 index 0000000..e39c07b --- /dev/null +++ b/src/Controller/HeadFootController.php @@ -0,0 +1,146 @@ + false]); + } + + $model = new Model($entityManager); + if($model->findWhateverNode('name_node', $params_array[0])){ + $node_data = $model->getNode()->getNodeData(); + + // liens réseaux sociaux + if(in_array($params_array[1], NodeData::$social_networks)){ + $social = $node_data->getData()['social'] ?? []; + $social[$params_array[1]] = $json['new_text']; + $node_data->updateData('social', $social); + } + // autres textes + else{ + $node_data->updateData($params_array[1], $json['new_text']); // $params_array[1] n'est pas contrôlé + } + + $entityManager->flush(); + return new JsonResponse(['success' => true]); + } + else{ + return new JsonResponse(['success' => false]); + } + } + static public function uploadAsset(EntityManager $entityManager, string $request_params): JsonResponse + { + if(empty($_FILES)){ + return new JsonResponse(['success' => false], JsonResponse::HTTP_BAD_REQUEST); // code 400 + } + else{ + if(!is_dir(Asset::USER_PATH)){ + mkdir(Asset::USER_PATH, 0777, true); + } + + /* -- téléchargement -- */ + $file = $_FILES['file']; + $allowed_extensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'tiff', 'tif', 'ico', 'bmp']; // pas de SVG + $extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); + if(!in_array($extension, $allowed_extensions) || $extension === 'jpg'){ + $extension = 'jpeg'; + } + $mime_type = mime_content_type($file['tmp_name']); + $hash = hash_file('sha256', $file['tmp_name']); + + /* -- instance d'Asset -- */ + $model = new Model($entityManager); + $result = $model->getWhatever('App\Entity\Asset', 'hash', $hash); + + if(count($result) > 0){ // asset existant trouvé + $asset = $result[0]; + + // correction des informations + $name = $asset->getFileName(); // permet à priori de réécrire par dessus le précédent fichier + //$asset->setFileName($name); + $asset->setMimeType($mime_type); + } + else{ + $name = Security::secureFileName(pathinfo($file['name'], PATHINFO_FILENAME)); + $name = uniqid($name . '_') . '.' . $extension; + $asset = new Asset($name, $mime_type, $hash); + } + + /* -- écriture du fichier sur le disque -- */ + if(!ImageUploadController::imagickCleanAndWriteImage(file_get_contents($file['tmp_name']), Asset::USER_PATH . $name)){ // recréer l’image pour la nettoyer + return new JsonResponse(['success' => false, 'message' => "Erreur de l'enregistrement de l'image: problème de permission ou format non valide.", 'format' => $extension], JsonResponse::HTTP_INTERNAL_SERVER_ERROR); // code 500 + } + else{ + $params_array = explode('_', $request_params); // head_favicon, header_logo, header_background, footer_logo + + /* -- table intermédiaire node_data/asset-- */ + if($model->findWhateverNode('name_node', $params_array[0])){ // noeud (head, header ou footer) + $node_data = $model->getNode()->getNodeData(); + + // recherche à l'aide du rôle + $old_nda = null; + foreach($node_data->getNodeDataAssets() as $nda){ + if($nda->getRole() === $request_params){ + $old_nda = $nda; + $old_nda->setAsset($asset); + break; + } + } + // entrée pas trouvée + if($old_nda === null){ + $new_nda = new AssetEmployment($node_data, $asset, $request_params); // $request_params sera le rôle de l'asset + $entityManager->persist($new_nda); + } + + if(count($result) === 0){ + $entityManager->persist($asset); + } + $entityManager->flush(); + return new JsonResponse(['success' => true, 'location' => Asset::USER_PATH . $name, 'mime_type' => $mime_type]); + } + else{ + return new JsonResponse(['success' => false, 'message' => "Erreur noeud non trouvé, c'est pas du tout normal!"], JsonResponse::HTTP_INTERNAL_SERVER_ERROR); // code 500 + } + } + } + } + + static public function displaySocialNetwork(EntityManager $entityManager, string $request_params, array $json): JsonResponse + { + $params_array = explode('_', $request_params); + if(count($params_array) !== 2){ + return new JsonResponse(['success' => false]); + } + + $model = new Model($entityManager); + if(in_array($params_array[1], NodeData::$social_networks) && $model->findWhateverNode('name_node', $params_array[0])){ + $node_data = $model->getNode()->getNodeData(); + $social_show = $node_data->getData()['social_show'] ?? []; + $social_show[$params_array[1]] = $json['checked']; + $node_data->updateData('social_show', $social_show); + + $entityManager->flush(); + return new JsonResponse(['success' => true, 'checked' => $json['checked']]); + } + else{ + return new JsonResponse(['success' => false]); + } + } +} \ No newline at end of file diff --git a/src/Controller/ImageUploadController.php b/src/Controller/ImageUploadController.php new file mode 100644 index 0000000..4ca9f48 --- /dev/null +++ b/src/Controller/ImageUploadController.php @@ -0,0 +1,194 @@ +readImageBlob($image_data); + $imagick->stripImage(); // nettoyage métadonnées + //$imagick->setImageFormat($format); // inutile, writeImage force la conversion + + // compression + switch($format){ + case 'jpeg': // particularité du switch, si 'jpeg' le test de 'jpg' est ignoré et on va jusqu'au break + case 'jpg': + $imagick->setImageCompression(\Imagick::COMPRESSION_JPEG); + $imagick->setImageCompressionQuality(85); + break; + case 'webp': + $imagick->setImageCompression(\Imagick::COMPRESSION_WEBP); + $imagick->setImageCompressionQuality(85); + break; + case 'png': + $imagick->setImageCompression(\Imagick::COMPRESSION_ZIP); + $imagick->setImageCompressionQuality(7); // 9 est sans perte + break; + case 'tiff': + $imagick->setImageCompression(\Imagick::COMPRESSION_LZW); // LZW est sans perte + break; + } + + // enregistrement + // writeImage utilise l'extension du fichier et ignore le format détecté + // imagemagick est à l'origine une appli console, elle considère que l'extension montre l'intention de l'utilisateur + $imagick->writeImage($local_path); // enregistrement + $imagick->clear(); + $imagick->destroy(); + return true; + } + catch(\Exception $e){ + return false; + } + } + static public function curlDownloadImage(string $url, int $maxRetries = 3, int $timeout = 10): string|false + { + $attempt = 0; + $imageData = false; + + while($attempt < $maxRetries){ + $ch = curl_init($url); // instance de CurlHandle + curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout); + curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); + curl_setopt($ch, CURLOPT_USERAGENT, 'TinyMCE-Image-Downloader'); + + $imageData = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + //$curlError = curl_error($ch); + + if($imageData !== false && $httpCode >= 200 && $httpCode < 300){ + return $imageData; + } + + $attempt++; + sleep(1); + } + + return false; // échec après trois tentatives + } + + // téléchargement par le plugin (bouton "insérer une image") + static public function imageUploadTinyMce(): JsonResponse + { + if(!isset($_FILES['file'])){ + return new JsonResponse(['message' => 'Erreur 400: Bad Request'], JsonResponse::HTTP_BAD_REQUEST); // code 400 + } + if(!is_uploaded_file($_FILES['file']['tmp_name'])) { + return new JsonResponse(['message' => "Le fichier n'a pas été téléchargé correctement."], JsonResponse::HTTP_BAD_REQUEST); // code 400 + } + + $dest = 'user_data/images/'; + $dest_mini = 'user_data/images-mini/'; + + // Vérifier si les répertoires existent, sinon les créer + if(!is_dir($dest)){ + mkdir($dest, 0755, true); + } + if(!is_dir($dest_mini)){ + mkdir($dest_mini, 0755, true); + } + + $name = Security::secureFileName(pathinfo($_FILES['file']['name'], PATHINFO_FILENAME)); + $extension = strtolower(pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION)); + $image_data = file_get_contents($_FILES['file']['tmp_name']); + if(!in_array($extension, self::ALLOWED_EXTENSIONS)){ + $extension = 'jpeg'; + } + $local_path = uniqid($dest . $name . '_') . '.' . $extension; + + // créer une miniature de l'image + // + + if(self::imagickCleanAndWriteImage($image_data, $local_path)){ // recréer l’image pour la nettoyer + return new JsonResponse(['location' => $local_path]); // renvoyer l'URL de l'image téléchargée + } + else{ + return new JsonResponse(['message' => 'Erreur image non valide'], JsonResponse::HTTP_INTERNAL_SERVER_ERROR); // code 500 + } + } + + // collage de HTML => recherche de balises , 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

+ + + + + + + + '; + foreach($data as $entry){ + $view .= ' + + + '; + } + $view .= '
date et heure (Greenwich)connexion réussie
' . $entry->getFormatedDate() . '' . ($entry->getSuccess() ? 'oui' : 'non') . '
'; + 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 "Image"; + } 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
à la création d'un article + public function makeSectionNode(): bool + { + $bulk_data = $this->entityManager + ->createQuery('SELECT n FROM App\Entity\Node n WHERE n.parent = :parent') + ->setParameter('parent', $this->node) + ->getResult(); + + foreach($bulk_data as $article){ + $this->node->addChild($article); // pas de flush, on ne va pas écrire dans la BDD à chaque nouvelle page + } + return true; + } + + public function findUniqueNodeByName(string $name): void // = unique en BDD, donc sans "page" associée + { + $bulk_data = $this->entityManager + ->createQuery('SELECT n FROM App\Entity\Node n WHERE n.name_node = :name') + ->setParameter('name', $name) + ->getResult(); + $this->node = $bulk_data[0]; + } + + public function findItsChildren(): void + { + $bulk_data = $this->entityManager + ->createQuery('SELECT n FROM App\Entity\Node n WHERE n.parent = :parent AND n.page = :page') + ->setParameter('parent', $this->node) + ->setParameter('page', $this->page) + ->getResult(); + foreach($bulk_data as $child){ + $this->node->addChild($child); + } + } + + private function getAllEmails(): array + { + $dql = 'SELECT e FROM App\Entity\Email e'; + return $this->entityManager + ->createQuery($dql) + ->getResult(); + } + /*private function getFormsNodeData(): array + { + $dql = 'SELECT fd FROM App\Entity\NodeData fd WHERE node.name_node = form'; + return $this->entityManager + ->createQuery($dql) + //->setParameter('page', $this->page) + ->getResult(); + }*/ + //private function getEmailsBySender(string $sender): array +} diff --git a/src/Model/Path.php b/src/Model/Path.php new file mode 100644 index 0000000..91eb2ff --- /dev/null +++ b/src/Model/Path.php @@ -0,0 +1,80 @@ +findPage(Model::$menu, $path_array); // remplit $this->current_page + } + catch(\Exception $e){} + } + + // produit un tableau de Page en comparant le chemin demandé avec les données dans Menu + // succès => une exception est lancée pour sortir des fonctions imbriquées + // echec => redirection vers la page erreur 404 + private function findPage(Page|Menu $menu, array $path_array): void + { + // recherche dans les autres pages + if($menu instanceof Menu){ + foreach($menu->getOtherPages() as $page){ + if($path_array[0] === $page->getEndOfPath()){ + $this->current_page[] = $page; + throw new \Exception(); + } + } + } + // recherche dans le menu + foreach($menu->getChildren() as $page){ + if($path_array[0] === $page->getEndOfPath()){ + $this->current_page[] = $page; + if(count($path_array) > 1){ + array_shift($path_array); // $this->path_array n'est pas modifié, un tableau PHP est passé à une fonction par copie + $this->findPage($page, $path_array); + } + else{ + throw new \Exception(); // sortir de tous les findPage() en même temps + } + } + } + + // rien trouvé + header('Location: '. new URL(['page' => Model::$menu->getChildren()[0]->getEndOfPath()])); + die; + } + + public function getString(): string + { + $path_string = ""; + foreach($this->current_page as $one_page){ + $path_string .= $one_page->getEndOfPath() . '/'; + } + return rtrim($path_string, '/'); + } + public function getArray(): array + { + return $this->current_page; + } + + // c'est là qu'on est quoi + public function getLast(): Page + { + return $this->current_page[count($this->current_page) - 1]; + } +} \ No newline at end of file diff --git a/src/Model/Position.php b/src/Model/Position.php new file mode 100644 index 0000000..7799cdf --- /dev/null +++ b/src/Model/Position.php @@ -0,0 +1,52 @@ +children); $i++) + { + $tmp = $this->children[$i]; + $j = $i - 1; + + // Déplacez les éléments du tableau qui sont plus grands que la clé à une position devant leur position actuelle + while($j >= 0 && $this->children[$j]->getPosition() > $tmp->getPosition()) { + $this->children[$j + 1] = $this->children[$j]; + $j--; + } + $this->children[$j + 1] = $tmp; + } + + // récursivité (utile dans Menu et chemin) + // appel dans Page et dans MenuAndPathController (un tri supplémentaire à faire?) + if(self::class === 'App\Entity\Page'){ + foreach($this->children as $child) { + if(count($child->children) > 0) { + $child->sortChildren($reindexation); + } + } + } + + if($reindexation){ + $this->reindexPositions(); + } + } + + // nouvelles positions (tableau $children => BDD) + public function reindexPositions(): void + { + $i = 1; + foreach($this->children as $child){ + $child->setPosition($i); + $i++; + } + } +} \ No newline at end of file diff --git a/src/Model/doctrine-bootstrap.php b/src/Model/doctrine-bootstrap.php new file mode 100644 index 0000000..13db87f --- /dev/null +++ b/src/Model/doctrine-bootstrap.php @@ -0,0 +1,34 @@ + Config::$db_driver, + 'user' => Config::$user, + 'password' => Config::$password, + 'host' => Config::$db_host, + 'dbname' => Config::$database, +], $config); + +// obtaining the entity manager +$entityManager = new EntityManager($connection, $config); + +foreach($entityManager->getMetadataFactory()->getAllMetadata() as $class){} \ No newline at end of file diff --git a/src/Service/Backup.php b/src/Service/Backup.php new file mode 100644 index 0000000..546e81e --- /dev/null +++ b/src/Service/Backup.php @@ -0,0 +1,272 @@ +format('Y-m-d') . '_' . $type . '.sql'; + + // les versions de mysql sont comme ci: 8.0.36 + // celles de mariadb sont comme ça: 10.11.6-MariaDB + $version = $entityManager->getConnection()->fetchOne('SELECT VERSION()'); + $engine = stripos($version, 'mariadb') !== false ? 'mariadb-dump' : 'mysqldump'; + + $tmp = tempnam('../var', 'tmp_db_codes_'); // crée un fichier avec un nom aléatoire et des droits 600 (concurrence) + // attention, si le dossier de destination n'est pas disponible, le fichier est placé avec les fichiers temporaires + file_put_contents($tmp, + "[client]\n + user=" . Config::$user . "\n + password=" . Config::$password . "\n + host=" . Config::$db_host . "\n"); + + $command = new Process([ + $engine, + '--defaults-extra-file=' . $tmp, // pour ne pas enregistrer les codes dans l'historique de la console ou dans les processus de l'OS + '--single-transaction', + '--quick', // évite d'exploser la RAM si beaucoup de données + '--result-file=' . $file_path, + Config::$database + ]); + + try{ + // unlink et chmod permettent que le serveur et l'utilisateur appelant bin/mysqldump.php réussissent + if(file_exists($file_path)){ + unlink($file_path); + } + $command->mustRun(); // comme run() mais lance une ProcessFailedException + + //$file_path = self::gzipCompress($file_path); // '.gz' ajouté à la fin + + chmod($file_path, 0666); + return $file_path; + } + finally{ + // exécuté même quand situé après "return" + unlink($tmp); + self::cleanBackups(); + } + } + + // compression gzip (gros gain de place sur le serveur), nécessite l'extension zlib + static public function gzipCompress(string $file_path): string + { + try{ + file_put_contents( + $file_path . '.gz', + gzencode(file_get_contents($file_path), 5), // plus rapide que 9 et taille identique d'après mes essais + ); + unlink($file_path); + $file_path .= '.gz'; + } + finally{ + return $file_path; + } + } + + static public function getBackupList(): array + { + $files = scandir(Backup::$backup_dir); // affiche un warning si échoue (à cacher en prod) + if(!$files){ + throw new \RuntimeException("Le serveur a rencontré une erreur:
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.

"; + } + define('TABLE_PREFIX', self::$table_prefix); + } + + // renseigner les variables internes de Config + static private function hydrate(array $raw_data): void + { + foreach($raw_data as $field => $value) + { + if($value != '') // valeur par défaut + { + if(isset(self::$$field)) // le champ existe dans Config + { + // problème du slash à la fin du nom d'un dossier + $value = self::slashAtEndOfPath($field, $value); + self::$$field = $value; + } + else + { + echo "debug: le fichier config.ini comporte une erreur, le champ: " . $field . " est incorrect,\nl'information contenue sur cette ligne ne sera pas utilisée\n"; + } + } + /*else + { + echo "debug: le champ " . $field . " est vide, la valeur par défaut " . self::$$field . " sera utilisée.\n"; + }*/ + } + } + + + // pour que les chemins finissent toujours par un / + static private function slashAtEndOfPath(string $field, string $value): string + { + foreach(self::$path_vars as $item) + { + if($field === $item){ + return !str_ends_with($value, '/') ? $value . '/' : $value; + } + } + return $value; + } +} diff --git a/src/Service/EmailService.php b/src/Service/EmailService.php new file mode 100644 index 0000000..38b4cf8 --- /dev/null +++ b/src/Service/EmailService.php @@ -0,0 +1,106 @@ + exceptions + $mail->CharSet = 'UTF-8'; + + $smtp_host = $form_data->getData()['smtp_host'] ?? Config::$smtp_host; + $smtp_secure = $form_data->getData()['smtp_secure'] ?? Config::$smtp_secure; + $smtp_username = $form_data->getData()['smtp_username'] ?? Config::$smtp_username; + $smtp_password = $form_data->getData()['smtp_password'] ?? Config::$smtp_password; + + $email_from = $form_data->getData()['email_from'] ?? Config::$email_from; // adresse de l'expéditeur affichée + $email_from = empty($email_from) ? $smtp_username : $email_from; // si vide + $email_from_name = $form_data->getData()['email_from_name'] ?? Config::$email_from_name; // = nom de l'expéditeur affiché + $email_dest = $form_data->getData()['email_dest'] ?? Config::$email_dest; + $email_dest_name = $form_data->getData()['email_dest_name'] ?? Config::$email_dest_name; // = destinataire formulaire + + try{ + // Paramètres du serveur + $mail->isSMTP(); + $mail->Host = $smtp_host; + $mail->SMTPAuth = true; + $mail->Port = 25; + + if($mail->SMTPAuth){ + $mail->Username = $smtp_username; // e-mail + $mail->Password = $smtp_password; + $mail->SMTPSecure = $smtp_secure; // tls (starttls) ou ssl (smtps) + if($mail->SMTPSecure === 'tls'){ + $mail->Port = 587; + } + elseif($mail->SMTPSecure === 'ssl'){ + $mail->Port = 465; + } + } + //var_dump($mail->smtpConnect());die; // test de connexion + + // Expéditeur et destinataire + // $email_from, $email_from_name et $email_dest_name sont modifiables uniquement dans le config.ini pour l'instant + $mail->setFrom(strtolower($email_from), $email_from_name); + $mail->addAddress(strtolower($email_dest), $email_dest_name); + + // Contenu + $mail->isHTML(true); + if($test_email){ + $mail->Subject = "TEST d'un envoi d'e-mail depuis le site web"; + } + else{ + $mail->Subject = 'Message envoyé par: ' . $name . ' (' . $email . ') depuis le site web'; + } + $mail->Body = $message; + $mail->AltBody = $message; + + $mail->send(); + + // copie en BDD + if(!$test_email && ($form_data->getData()['keep_emails'] ?? self::KEEP_EMAILS_DEFAULT)){ + $db_email = new Email($name, $email, $email_dest, $message, $form_data); + $entityManager->persist($db_email); + self::updateLastContactDate($entityManager, $email); + $entityManager->flush(); + } + + return true; + } + catch(\Exception $e){ + echo "Le message n'a pas pu être envoyé. Erreur : {$e}
{$mail->ErrorInfo}"; + return false; + } + } + + static public function updateLastContactDate(EntityManager $entityManager, string $sender): void + { + foreach($entityManager->getRepository('App\Entity\Email')->findAll() as $email){ + $email->getSenderAddress() === $sender ? $email->updateLastContactDate() : null; + } + } + + // peut être appelée par bin/clean_emails_cron.php + static public function cleanEmails(EntityManager $entityManager): void + { + $emails = $entityManager->getRepository('App\Entity\Email')->findAll(); + foreach($emails as $email){ + if($email->getDeletionDate() < new \DateTime()){ + $entityManager->remove($email); + } + } + $entityManager->flush(); + } +} \ No newline at end of file diff --git a/src/Service/FormValidation.php b/src/Service/FormValidation.php new file mode 100644 index 0000000..d91aa12 --- /dev/null +++ b/src/Service/FormValidation.php @@ -0,0 +1,216 @@ + 'emailSendStrategy', + self::EmailParam => 'emailParamStrategy', + self::CreateUser => 'createUserStrategy', + self::Connection => 'connectionStrategy', + self::UsernameUpdate => 'usernameUpdateStrategy', + self::PasswordUpdate => 'passwordUpdateStrategy' + }; + } +} + +class FormValidation +{ + private array $data; // tableau associatif (probablement $_POST) + private \Closure $callStrategy; + private ?string $error; + + public function __construct(array $data, ValidationStrategy $strategy){ + $this->data = $data; + $method = $strategy->method(); + $this->callStrategy = fn() => $this->$method(); + } + + public function validate(): bool + { + $this->error = ($this->callStrategy)(); // appel de la callback + return empty($this->errors); + } + + public function getError(): string + { + return $this->error; + } + + public function getField(string $field): string + { + return $this->data[$field] ?? ''; + } + + // méthodes de validation + private function captchaValidate(bool $clean_session = true): ?string + { + $captcha_solution = (isset($_SESSION['captcha']) && is_int($_SESSION['captcha'])) ? $_SESSION['captcha'] : 0; + $captcha_try = isset($this->data['captcha']) ? Captcha::controlInput($this->data['captcha']) : 0; + if($clean_session){ + unset($_SESSION['captcha']); + } + + if($captcha_try == 0){ + return 'error_non_valid_captcha'; + } + elseif($captcha_solution == 0){ // ne peut pas arriver, si? + return 'captcha_server_error'; + } + elseif($captcha_try !== $captcha_solution){ + return 'bad_solution_captcha'; + } + return null; + } + + // 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)); + } + + + // stratégies + private function emailSendStrategy(): ?string + { + if($error = $this->captchaValidate(false)){ + return $error; + } + + if(empty($this->data['name']) || empty($this->data['email']) || empty($this->data['message']) + || !isset($this->data['hidden']) || !empty($this->data['hidden'])){ + return 'missing_fields'; + } + + elseif(!filter_var(trim($this->data['email']), FILTER_VALIDATE_EMAIL)){ + return 'bad_email_address'; + } + + $this->data['name'] = htmlspecialchars(trim($this->data['name'])); + $this->data['email'] = htmlspecialchars(trim($this->data['email'])); + $this->data['message'] = htmlspecialchars($this->data['message']); + + return null; + } + private function emailParamStrategy(): ?string + { + if(empty($this->data['id']) || empty($this->data['what_param']) || empty($this->data['value']) + || !isset($this->data['hidden']) || !empty($this->data['hidden'])){ + return 'missing_fields'; + } + + if(!in_array($this->data['what_param'], ['smtp_host', 'smtp_secure', 'smtp_username', 'smtp_password', 'email_dest'])){ + return 'unknown_parameter'; + } + elseif($this->data['what_param'] === 'smtp_username' || $this->data['what_param'] === 'email_dest'){ + if(!filter_var($this->data['value'], FILTER_VALIDATE_EMAIL)){ + return 'invalide_email_address'; + } + } + return null; + } + private function createUserStrategy(): ?string + { + if($error = $this->captchaValidate()){ + return $error; + } + + // test mauvais paramètres + if(empty($this->data['login']) || empty($this->data['password']) || empty($this->data['password_confirmation']) + || !isset($this->data['create_user_hidden']) || !empty($this->data['create_user_hidden'])) + { + return 'bad_login_or_password'; + } + + if($this->data['password'] !== $this->data['password_confirmation']){ + return 'different_passwords'; + } + + if($this->data['login'] !== self::removeSpacesTabsCRLF(htmlspecialchars($this->data['login'])) + || $this->data['password'] !== self::removeSpacesTabsCRLF(htmlspecialchars($this->data['password']))){ + return 'forbidden_characters'; + } + return null; + } + private function connectionStrategy(): ?string + { + if($error = $this->captchaValidate()){ + return $error; + } + + if(empty($this->data['login']) || empty($this->data['password']) + || !isset($this->data['connection_hidden']) || !empty($this->data['connection_hidden'])) + { + return 'bad_login_or_password'; + } + return null; + } + private function usernameUpdateStrategy(): ?string + { + if($error = $this->captchaValidate()){ + return $error; + } + + if(empty($this->data['login']) || empty($this->data['password']) || empty($this->data['new_login']) + || !isset($this->data['modify_username_hidden']) || !empty($this->data['modify_username_hidden'])) + { + return 'bad_login_or_password'; + } + + $new_login = self::removeSpacesTabsCRLF(htmlspecialchars($this->data['new_login'])); + if($new_login !== $this->data['new_login']){ + return 'forbidden_characters'; + } + + if($this->data['login'] !== $_SESSION['user']['username']){ + return 'bad_login_or_password'; + } + if($this->data['login'] === $new_login){ + return 'same_username_as_before'; + } + return null; + } + private function passwordUpdateStrategy(): ?string + { + if($error = $this->captchaValidate()){ + return $error; + } + + if(empty($this->data['login']) || empty($this->data['password']) || empty($this->data['new_password']) + || !isset($this->data['modify_password_hidden']) || !empty($this->data['modify_password_hidden'])) + { + return 'bad_login_or_password'; + } + + $new_password = self::removeSpacesTabsCRLF(htmlspecialchars($this->data['new_password'])); + if($new_password !== $this->data['new_password']){ + return 'forbidden_characters'; + } + + if($this->data['login'] !== $_SESSION['user']['username']){ + return 'bad_login_or_password'; + } + if($this->data['password'] === $new_password){ + return 'same_password_as_before'; + } + return null; + } +} \ No newline at end of file diff --git a/src/Service/Installation.php b/src/Service/Installation.php new file mode 100644 index 0000000..ed9f20a --- /dev/null +++ b/src/Service/Installation.php @@ -0,0 +1,109 @@ +L'extension " . $extension . " est manquante.

"); + $flag = true; + } + } + if(!class_exists(\DOMDocument::class)){ // théoriquement plus fiable que extension_loaded() + echo("

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.

'; + die; + } + } + + static public function checkFilesAndFoldersRights(): void + { + // -- droits des fichiers et dossiers -- + $droits_dossiers = 0777; + + $flag = false; + if(!file_exists('user_data')){ + try{ + mkdir('user_data/'); + chmod('user_data/', $droits_dossiers); + } + catch(\Exception $e){ + echo '

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

'; + echo $e; + $flag = true; + } + } + if(!file_exists('../var')){ + try{ + mkdir('../var'); + chmod('../var', $droits_dossiers); + } + catch(\Exception $e){ + echo $e; + $flag = true; + } + } + if(!file_exists('../var/backups')){ + try{ + mkdir('../var/backups'); + chmod('../var/backups', $droits_dossiers); // autoriser à la fois le serveur et les scripts dans bin/ + } + catch(\Exception $e){ + echo $e; + $flag = true; + } + } + + // droits 600 pour celui-ci + if(!file_exists('../config/config.ini')){ + // aide à la création du config.ini + echo '

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 + case 'upload_image_url': + return ImageUploadController::uploadImageHtml(); + // collage d'une image (code base64 dans le presse-papier) non encapsulée dans du HTML + case 'upload_image_base64': + return ImageUploadController::uploadImageBase64(); + + /* -- requêtes spécifiques au calendrier -- */ + case 'new_event': + return CalendarController::newEvent($json, $this->entityManager); + case 'update_event': + return CalendarController::updateEvent($json, $this->entityManager); + case 'remove_event': + return CalendarController::removeEvent($json, $this->entityManager); + + /* -- mode maintenance -- */ + case 'get_logs': + return MaintenanceController::getLogs($this->entityManager); + case 'erase_logs': + return MaintenanceController::eraseLogs($this->entityManager); + + default: + return new JsonResponse(['success' => false, 'error' => 'bad parameters'], JsonResponse::HTTP_BAD_REQUEST); + } + } + + /* -- site entier (header, footer, favicon) -- */ + elseif($this->request->query->has('head_foot_text')){ + return HeadFootController::setTextData($this->entityManager, $this->request->query->get('head_foot_text'), $json); + } + elseif($this->request->query->has('head_foot_social_check')){ + return HeadFootController::displaySocialNetwork($this->entityManager, $this->request->query->get('head_foot_social_check'), $json); + } + + /* -- page Menu et chemins -- */ + elseif($this->request->query->has('menu_edit')){ + // ne suit pas la règle, faire ça dans un contrôleur? + Model::$menu = new Menu($this->entityManager); // récupération des données + + // flèche gauche <=: position = position du parent + 1, parent = grand-parent, recalculer les positions + if($this->request->query->get('menu_edit') === 'move_one_level_up' && isset($json['id'])){ + return MenuAndPathsController::MoveOneLevelUp($this->entityManager, $json); + } + // flèche droite =>: position (léments de a fraterie + 1, l'élément précédent devient le parent + elseif($this->request->query->get('menu_edit') === 'move_one_level_down' && isset($json['id'])){ + return MenuAndPathsController::MoveOneLevelDown($this->entityManager, $json); + } + elseif($this->request->query->get('menu_edit') === 'switch_positions' && isset($json['id1']) && isset($json['id2'])){ + return MenuAndPathsController::switchPositions($this->entityManager, $json); + } + elseif($this->request->query->get('menu_edit') === 'display_in_menu' && isset($json['id']) && isset($json['checked'])){ + return MenuAndPathsController::displayInMenu($this->entityManager, $json); + } + elseif($this->request->query->get('menu_edit') === 'url_edit' && isset($json['id']) && isset($json['field']) && isset($json['input_data'])){ + return MenuAndPathsController::editUrl($this->entityManager, $json); + } + else{ + return new JsonResponse(['success' => false, 'error' => 'bad parameters'], JsonResponse::HTTP_BAD_REQUEST); // code 400 + } + } + + /* -- mode Modification d'une page -- */ + // partie "page" + elseif($this->request->query->has('page_edit')){ + switch($this->request->query->get('page_edit')){ + case 'page_title': + return PageManagementController::setPageTitle($this->entityManager, $json); + case 'page_description': + return PageManagementController::setPageDescription($this->entityManager, $json); + default: + return new JsonResponse(['success' => false, 'error' => 'bad parameters'], JsonResponse::HTTP_BAD_REQUEST); // code 400 + } + } + + // partie "blocs" + elseif($this->request->query->has('bloc_edit')){ + switch($this->request->query->get('bloc_edit')){ + case 'rename_page_bloc': + return PageManagementController::renameBloc($this->entityManager, $json); + case 'switch_blocs_positions': + return PageManagementController::SwitchBlocsPositions($this->entityManager, $json, $this->request->query->get('page')); + case 'change_articles_order': + return PageManagementController::changeArticlesOrder($this->entityManager, $json); + case 'change_presentation': + return PageManagementController::changePresentation($this->entityManager, $json); + case 'change_cols_min_width': + return PageManagementController::changeColsMinWidth($this->entityManager, $json); + case 'change_pagination_limit': + return PageManagementController::changePaginationLimit($this->entityManager, $json); + default: + return new JsonResponse(['success' => false, 'error' => 'bad parameters'], JsonResponse::HTTP_BAD_REQUEST); // code 400 + } + } + + else{ + return new JsonResponse(['success' => false, 'error' => 'bad parameters'], JsonResponse::HTTP_BAD_REQUEST); // code 400 + } + } + + /* -- upload avec FormData OU formulaire HTML avec fichier -- */ + elseif(str_starts_with($this->request->headers->get('Content-Type'), 'multipart/form-data')){ // = $_SERVER['CONTENT_TYPE'] + // dans tinymce avec le plugin (bouton "insérer une image" de l'éditeur ou glisser-déposer) + if($this->request->query->get('action') === 'upload_image_tinymce'){ + return ImageUploadController::imageUploadTinyMce(); + } + // dans tinymce, des quatre méthodes: bouton "link", drag & drop, html, base64 + elseif($this->request->query->get('action') === 'upload_file_tinymce'){ + return FileUploadController::fileUploadTinyMce(); + } + elseif($this->request->query->has('head_foot_image')){ + return HeadFootController::uploadAsset($this->entityManager, $this->request->query->get('head_foot_image')); + } + + /* -- page Maintenance -- */ + elseif($this->request->query->get('action') === 'restore_database' && $this->request->request->get('hidden') === '' + && $this->request->files->has('uploaded_sql')){ + return MaintenanceController::downloadSQL($this->entityManager, $this->request); + } + else{ + // choix ici entre répondre en JSON ou par une redirection, choix du JSON pour pouvoir passer un message + return new JsonResponse(['success' => false, 'error' => 'bad parameters'], JsonResponse::HTTP_BAD_REQUEST); // code 400 + } + } + + /* -- formulaire HTML sans fichier -- */ + elseif($this->request->headers->get('Content-Type') === 'application/x-www-form-urlencoded'){ + if($this->request->query->get('action') === 'delete_article' && $this->request->query->has('id')){ + return ArticleController::deleteArticle($this->entityManager, $this->request); // version formulaire + } + + /* -- nouvelle page -- */ + elseif($this->request->request->get('page_name') !== null + && $this->request->request->get('page_name_path') !== null + && $this->request->request->get('page_location') !== null + && $this->request->request->get('page_description') !== null + && $this->request->request->get('new_page_hidden') === ''){ + return PageManagementController::newPage($this->entityManager, $this->request->request); + } + + /* -- suppression d'une page -- */ + elseif($this->request->request->get('page_id') !== null + && $this->request->request->get('submit_hidden') === ''){ + return PageManagementController::deletePage($this->entityManager, $this->request->request->get('page_id')); + } + + + /* -- mode Modification d'une page -- */ + // modification du chemins en snake_case + elseif($this->request->request->get('page_menu_path') !== null + && $this->request->request->get('page_id') !== null + && $this->request->request->get('page_name_path_hidden') === ''){ + return PageManagementController::updatePageMenuPath($this->entityManager, $this->request->request->get('page_menu_path')); + } + // ajout d'un bloc dans une page + elseif($this->request->request->get('bloc_title') !== null + && $this->request->request->get('bloc_select') !== null + && $this->request->request->get('bloc_title_hidden') === ''){ // contrôle anti-robot avec input hidden + return PageManagementController::addBloc($this->entityManager, $this->request); + } + // suppression d'un bloc de page + elseif($this->request->request->get('delete_bloc_id') !== null + && $this->request->request->get('delete_bloc_hidden') === ''){ // contrôle anti-robot avec input hidden + return PageManagementController::deleteBloc($this->entityManager, $this->request); + } + + + /* -- page Menu et chemins -- */ + // création d'une entrée de menu avec une URL + elseif($this->request->request->has("label_input") && $this->request->request->has("url_input") && $this->request->request->has("location")){ + return MenuAndPathsController::newUrlMenuEntry($this->entityManager); + } + // suppression d'une entrée de menu avec une URL + elseif($this->request->request->has('delete') && $this->request->request->has('x') && $this->request->request->has('y')){ // 2 params x et y sont là parce qu'on a cliqué sur une image + return MenuAndPathsController::deleteUrlMenuEntry($this->entityManager); + } + + + /* -- page Mon compte -- */ + elseif($this->request->query->get('action') === 'update_username'){ + return UserController::updateUsername($this->entityManager); + } + elseif($this->request->query->get('action') === 'update_password'){ + return UserController::updatePassword($this->entityManager); + } + + /* -- page Maintenance -- */ + elseif($this->request->query->get('action') === 'restore_database' && $this->request->get('hidden') === '' && $this->request->request->has('selected_sql')){ + return MaintenanceController::handleBackupSelection($this->entityManager, $this->request); + } + + // redirection page d'accueil + return new RedirectResponse((string)new URL(['error' => 'paramètres inconnus'])); + } + + // requêtes XMLHttpRequest + elseif($this->request->isXmlHttpRequest()){ + return new JsonResponse(['success' => false]); // noyer le poisson en laissant penser que le site gère les requêtes XHR + } + + // POST admin ne matchant pas + return new Response('bad parameters', Response::HTTP_BAD_REQUEST); // code 400 + } + + // POST non-admin ne matchant pas + return new Response('bad parameters', Response::HTTP_BAD_REQUEST); // code 400 + } + + // méthode HTTP inconnue + else{ + return new RedirectResponse((string)new URL(['error' => 'tu fais quoi là mec?'])); + } + } +} \ No newline at end of file diff --git a/src/Service/Security.php b/src/Service/Security.php new file mode 100644 index 0000000..0fbd8b7 --- /dev/null +++ b/src/Service/Security.php @@ -0,0 +1,112 @@ +1, // protection contre les élements et attributs dangereux + + // liste blanche d'éléments HTML + 'elements'=> 'h1, h2, h3, h4, h5, h6, p, br, s, em, span, strong, a, ul, ol, li, sup, sub, code, blockquote, div, pre, table, caption, colgroup, col, tbody, tr, th, td, figure, img, figcaption, iframe, small', + + // liste noire d'attributs HTML + 'deny_attribute'=> 'id, class' // on garde 'style' + ); + // faire qu'un certain élément puisse n'avoir que certains attributs, regarder la doc + private static $specHtmLawed = ''; + + // obtenir du HTML non dangereur sans appliquer htmlspecialchars + public static function secureHTML(string $chaine): string + { + return trim(htmLawed($chaine, self::$configHtmLawed, self::$specHtmLawed)); + } + + public static function secureFileName(string $chaine): string + { + // sécuriser un nom avec chemin avec basename? + //$chaine = basename($chaine); + + /* + - caractères interdits sous windows / \ : * ? " < > | + - mac autorise les / + - mac interdit : + - linux autorise tout sauf les / + - imagemagick ne supporte pas les : + + - 'espace' fonctionne + - / remplacé par firefox en : + - \ retire ce qui est devant le \ + - * fonctionne + - ? permet le téléchargement mais pas l'affichage + - " ne fonctionne pas, remplacé par %22, filtrer %22 + - < > fonctionnent + - | fonctionne + - = fonctionne, mais je filtre parce qu'on en trouve dans une URL + - ' ` fonctionnent + - % fonctionne + - (){}[] fonctionnent + - ^ fonctionne + - # ne fonctionne pas + - ~ fonctionne + - & fonctionne + - ^ pas encore testé + */ + + // => on remplace tout par des _ + // filtrer / et \ semble inutile + + /*$cibles = [' ', '/', '\\', ':', '*', '?', '<', '>', '|', '=', "'", '`', '"', '%22', '#']; + $chaine = str_replace($cibles, '_', $chaine); // nécéssite l'extension mbstring + $chaine = mb_strtolower($chaine); + return($chaine);*/ + + $chaine = preg_replace('/[^a-zA-Z0-9_-]/', '_', $chaine); // ne garder que les lettres, chiffres, tirets et underscores + $chaine = preg_replace('/_+/', '_', $chaine); // doublons d'underscores + return trim($chaine, '_'); + + // les problèmes avec \ persistent !! + // => javascript + // malheureusement document.getElementById('upload').files[0].name = chaine; ne marche pas! interdit! + // javascript ne doit pas pouvoir accéder au système de fichiers + // solutions: + // - au lieu de fournir une chaine (le chemin du fichier), donner un objet à files[0].name + // - créer une copie du fichier et l'envoyer à la place + // - envoyer le fichier en AJAX + // - envoyer le nom du fichier à part puis renommer en PHP + } +} + +// lien sans http:// +function fixLinks($data) +{ + // 1/ + // si une adresse est de type "domaine.fr" sans le http:// devant, le comportement des navigateurs est de rechercher un fichier comme si mon adresse commençait par file:// + // tomber ainsi sur une page d'erreur est parfaitement déroutant + + // regex pour détecter les balises
et ajouter http:// au début des liens si nécessaire + $pattern = '#(]+href=")((?!https?://)[^>]+>)#'; + //$data = preg_replace($pattern, '$1http://$2', $data); + + // 2/ + // cas où la regex fait mal son boulot: + // l'erreur 404 est gérée par le .htaccess + // et le visiteur est redirigé à la page "menu" + // (ça ne règle pas le problème mais c'est mieux) + + // 3/ + // quand l'éditeur est ouvert (avant de valider l'article), + // le lien qu'on vient de créer apparaît dans l'infobulle, + // cliquer dessus ouvre un onglet sur une erreur 404 + // solution partielle avec le .htaccess + // + // solution? fermer ce nouvel onglet avec echo ''; + // comment déclencher le JS? en faisant qu'une erreur 404 causée pour cette raison soit particulière? + + return($data); +} diff --git a/src/Service/URL.php b/src/Service/URL.php new file mode 100644 index 0000000..035f572 --- /dev/null +++ b/src/Service/URL.php @@ -0,0 +1,90 @@ +params = $gets; + if($anchor != ''){ + $this->setAnchor($anchor); + } + } + + // setters statiques + static public function setProtocol(string $protocol = 'http'): void + { + self::$protocol = $protocol === 'https' ? 'https://' : 'http://'; + } + static public function setPort(int|string $port = 80): void + { + if((int)$port === 443){ + self::$protocol = 'https://'; + self::$port = ''; + } + elseif((int)$port === 80){ + self::$protocol = 'http://'; + self::$port = ''; + } + else{ + self::$port = ':' . (string)$port; + } + } + static public function setHost(string $host): void + { + self::$host = $host; + } + static public function setPath(string $path): void + { + self::$path = '/' . ltrim($path, '/'); + } + + //setters normaux + public function addParams(array $gets): void + { + // array_merge est préféré à l'opérateur d'union +, si une clé existe déjà la valeur est écrasée + $this->params = array_merge($this->params, $gets); + } + public function setAnchor(string $anchor = ''): void + { + if($anchor != ''){ + $this->anchor = '#' . ltrim($anchor, '#'); + } + else{ + $this->anchor = ''; + } + } + + private function makeParams(): string + { + $output = ''; + $first = true; + + foreach($this->params as $key => $value) { + if($first){ + $output .= '?'; + $first = false; + } + else{ + $output .= '&'; + } + $output .= $key . '=' . $value; + } + return $output; + } + + public function __toString(): string + { + return self::$protocol . self::$host . self::$port . self::$path . $this->makeParams() . $this->anchor; + } +} \ No newline at end of file diff --git a/src/Service/User.php b/src/Service/User.php new file mode 100644 index 0000000..bd4c315 --- /dev/null +++ b/src/Service/User.php @@ -0,0 +1,25 @@ +createQuery("SELECT u FROM App\Entity\User u") + ->setMaxResults(1) + ->getOneOrNullResult()) + { + unset($_SESSION['user']); + return false; + } + else{ + return true; + } + } +} \ No newline at end of file diff --git a/src/Service/session.php b/src/Service/session.php new file mode 100644 index 0000000..39c6ba2 --- /dev/null +++ b/src/Service/session.php @@ -0,0 +1,94 @@ + 'session_invalide'])); + die; + } + + // MAJ de la session avec CERTAINES données + $_SESSION['user']['username'] = $user->getLogin(); + $_SESSION['user']['role'] = $user->getRole(); + + $is_admin = $user->getRole() === 'admin'; + } + + define('IS_ADMIN', $is_admin); + + // si on a un jour besoin d'une variable globale au lieu d'une constante + //$GLOBALS['is_admin'] = $is_admin; // version modifiable 1 + /*function isAdmin(): bool { // version modifiable 2 + return $_SESSION['user']['role'] ?? null === 'admin'; + }*/ + + + // => système de cache à ajouter pour ne pas lire la BDD à chaque fois + //remplacer ce qui est en haut + /*$user = $_SESSION['user'] ?? null; + if (!$user) { + // visiteur + } + // Vérification périodique (ex: toutes les 5 minutes) + if (time() - $user['last_check'] > 300) { + $user = UserController::getUserById($user['id'], $entityManager); + if (!$user) { + session_destroy(); + header('Location: /login.php'); + exit; + } + // cache pour ne pas avoir à lire la BDD à chaque page + $_SESSION['user'] = [ + 'id' => $user['id'], + 'role' => $user['role'], + 'username' => $user['username'], + 'last_check' => time() + ]; + $user = $_SESSION['user']; + } + $is_admin = ($user['role'] === 'admin');*/ + + + // améliorations possibles: ajouter expiration automatique + protection contre vol de session (IP / user-agent) sans casser ton app. +} + +// nettoyage complet +/*function cleanSession(){ + unset($_SESSION['user']); // mémoire vive + session_destroy(); // fichier côté serveur + setcookie('PHPSESSID', '', time() - 86400, '/'); // cookie de session +}*/ \ No newline at end of file diff --git a/src/View/AbstractBuilder.php b/src/View/AbstractBuilder.php new file mode 100644 index 0000000..170e871 --- /dev/null +++ b/src/View/AbstractBuilder.php @@ -0,0 +1,81 @@ +id_node = $node->getId(); + } + + protected function useChildrenBuilder(Node $node): void + { + foreach($node->getChildren() as $child_node) + { + // le double \\ permet de ne pas échapper la quote + $builder_name = 'App\View\\' . $this->snakeToPascalCase($child_node->getName()) . 'Builder'; + $builder = new $builder_name($child_node); + $this->html .= $builder->render(); + + // pages spéciales où on n'assemble pas tout + if($builder_name === 'HeadBuilder' && in_array(Model::$page_path->getString(), ['connection', 'user_edit'])) + { + foreach($node->getChildren() as $target_node){ + if($target_node->getName() === 'main'){ + $main_node = $target_node; + break; + } + } + // on construit
et on s'arrête! les autres noeuds sont ignorés + $builder_name = $this->snakeToPascalCase($main_node->getName()) . 'Builder'; + $builder = new $builder_name($main_node); + $this->html .= "\n"; + $this->html .= $builder->render() . "\n"; + $this->html .= "\n"; + break; + } + } + } + + private function snakeToPascalCase(string $input): string + { + return str_replace('_', '', ucwords($input, '_')); + } + + public function render(): string // = getHTML(), nécéssite d'être public + { + return $this->html; + } + protected function addHTML(string $html): void + { + $this->html .= $html; + } + + protected function insertSVG(string $path, array $attributes = []): string + { + $svg = file_get_contents($path); + + // modification des attributs + if(!empty($attributes)){ + $dom = new \DOMDocument(); + $dom->loadXML($svg); + $svg_elem = $dom->documentElement; + foreach($attributes as $key => $value){ + $svg_elem->setAttribute($key, $value); + } + $svg = $dom->saveXML($svg_elem); + } + return $svg; + } +} \ No newline at end of file diff --git a/src/View/BreadcrumbBuilder.php b/src/View/BreadcrumbBuilder.php new file mode 100644 index 0000000..81df0eb --- /dev/null +++ b/src/View/BreadcrumbBuilder.php @@ -0,0 +1,53 @@ +html = $this->breadcrumbHTML(); + } + + private function breadcrumbHTML(): string + { + $asset = 'assets/home.svg'; // => BDD? + $breadcrumb_array = Model::$page_path->getArray(); // tableau de Page + $html = ''; + $nb_of_entries = count($breadcrumb_array); + + if($nb_of_entries > 1) + { + // petite maison et flèche + $html .= '\n"; + } + return $html; + } +} \ No newline at end of file diff --git a/src/View/CalendarBuilder.php b/src/View/CalendarBuilder.php new file mode 100644 index 0000000..34b1952 --- /dev/null +++ b/src/View/CalendarBuilder.php @@ -0,0 +1,31 @@ +getName() . '.php'; + + $calendar_js_file = IS_ADMIN ? 'calendar_admin' : 'calendar'; + + if(file_exists($viewFile)) + { + if(!empty($node->getNodeData()->getData())) + { + extract($node->getNodeData()->getData()); + } + + ob_start(); + require $viewFile; + $this->html = ob_get_clean(); // pas de concaténation ici, on écrase + } + } +} \ No newline at end of file diff --git a/src/View/FooterBuilder.php b/src/View/FooterBuilder.php new file mode 100644 index 0000000..72a571d --- /dev/null +++ b/src/View/FooterBuilder.php @@ -0,0 +1,122 @@ +getName() . '.php'; + + if(file_exists($viewFile)) + { + $node_data = $node->getNodeData(); + // nom du contact, adresse et e-mail + if(!empty($node_data->getData())) + { + extract($node_data->getData()); + } + + $footer_logo = Asset::USER_PATH . $node_data->getAssetByRole('footer_logo')?->getFileName() ?? ''; + + $this->useChildrenBuilder($node); + $breadcrumb = $this->html; + + $empty_admin_zone = ''; + if(IS_ADMIN) + { + // données du footer + $admin_footer_name = ' + + + '; + $admin_footer_address = ' + + + '; + $admin_footer_email = ' + + + '; + + $admin_footer_logo = ' + + + '; + + // zone admin + $empty_admin_zone = 'empty_admin_zone'; + if(MainBuilder::$modif_mode){ + $mode = 'modification de page'; + $div_admin = 'logged_in modif_mode'; + } + else{ + $mode = 'administrateur'; + $div_admin = 'logged_in'; + } + + $link_change_password = new URL(['page' => 'user_edit', 'from' => CURRENT_PAGE]); + isset($_GET['id']) ? $link_change_password->addParams(['id' => $_GET['id']]) : ''; + + $link_logout = new URL(['action' => 'deconnection', 'from' => CURRENT_PAGE]); + isset($_GET['id']) ? $link_logout->addParams(['id' => $_GET['id']]) : ''; + + $zone_admin = '
+

Vous êtes en mode ' . $mode . ".

\n" . + '' . "\n" . + $this->makePageModifModeButton() . "\n" . + '' . "\n" . + '' . "\n" . + '' . "\n" . + '' . "\n" . + '
' . "\n"; + } + else + { + $div_admin = 'logged_out'; + $url = new URL(['page' => 'connection', 'from' => CURRENT_PAGE]); + if(Model::$page_path->getLast()->getEndOfPath() === 'article' && isset($_GET['id'])){ + $url->addParams(['id' => $_GET['id']]); + } + $zone_admin = ''; + + $admin_footer_name = ''; + $admin_footer_address = ''; + $admin_footer_email = ''; + $admin_footer_logo = ''; + } + + ob_start(); + require $viewFile; + $this->html = ob_get_clean(); + } + } + + private function makePageModifModeButton(): string + { + $link_edit_page = new URL(['page' => CURRENT_PAGE]); + if(!in_array(CURRENT_PAGE, ViewController::SPECIAL_PAGES)) // cas impossibles: 'user_edit' et 'connection' qui n'ont pas de footer + { + if(MainBuilder::$modif_mode){ + $link_edit_label = 'Sortir du mode modification'; + } + else{ + $link_edit_page->addParams(['mode' => 'page_modif']); + $link_edit_label = 'Modifier la page'; + } + return '
'; + } + else{ + return ''; + } + } +} \ No newline at end of file diff --git a/src/View/FormBuilder.php b/src/View/FormBuilder.php new file mode 100644 index 0000000..df5687d --- /dev/null +++ b/src/View/FormBuilder.php @@ -0,0 +1,55 @@ +getNodeData()->getData())){ + extract($node->getNodeData()->getData()); + } + + // un seul captcha à la fois! + if(!self::$captcha){ + self::$captcha = new Captcha; + $_SESSION['captcha'] = self::$captcha->getSolution(); + } + + // données stockées en vrac dans du JSON et récupérées avec extract => changer ça un jour + $smtp_host = $smtp_host ?? Config::$smtp_host; + $smtp_secure = $smtp_secure ?? Config::$smtp_secure; + $smtp_username = $smtp_username ?? Config::$smtp_username; + $email_dest = $email_dest ?? Config::$email_dest; + $keep_emails = (bool)($keep_emails ?? false); // (bool) est inutile mais plus clair + $retention_period = $this->getRetentionPeriod($retention_period ?? null, Email::DEFAULT_RETENTION_PERIOD); + $retention_period_sensible = $this->getRetentionPeriod($retention_period_sensible ?? null, Email::DEFAULT_RETENTION_PERIOD_SENSITIVE); + + $admin_content = ''; + if(IS_ADMIN){ + ob_start(); + require self::VIEWS_PATH . 'form_admin.php'; + $admin_content = ob_get_clean(); + } + + ob_start(); + require self::VIEWS_PATH . $node->getName() . '.php'; + $this->html = ob_get_clean(); // pas de concaténation ici, on écrase + } + + private function getRetentionPeriod(mixed $period, int $default_period): int + { + return ($period === null || (int)$period <= 0) ? $default_period : (int)$period; // (int) est nécessaire à cause du stockage JSON + } +} \ No newline at end of file diff --git a/src/View/GaleryBuilder.php b/src/View/GaleryBuilder.php new file mode 100644 index 0000000..436cc36 --- /dev/null +++ b/src/View/GaleryBuilder.php @@ -0,0 +1,72 @@ +getName() . '.php'; + + if(file_exists($viewFile)) + { + if(!empty($node->getNodeData()->getData())) + { + extract($node->getNodeData()->getData()); + } + + // ajouter un article + $new_article = ''; + if(IS_ADMIN) + { + $id = 'n' . $this->id_node; + $js = 'onclick="openEditor(\'' . $id . '\')"'; + + $share_button = ''; + $html = ''; + + $new_button = '

' . "\n" . + '

'; + + $modify_js = 'onclick="openEditor(\'' . $id . '\')"'; + $modify_article = '' . "\n"; + + $up_js = 'onclick="switchPositions(\'' . $id . '\', \'up\')"'; + $up_button = '' . "\n"; + + $down_js = 'onclick="switchPositions(\'' . $id . '\', \'down\')"'; + $down_button = '' . "\n"; + + $delete_js = 'onclick="deleteArticle(\'' . $id . '\')"'; + $delete_article = '' . "\n"; + + $close_js = 'onclick="closeEditor(\'' . $id . '\')"'; + $close_editor = ''; + + $submit_js = 'onclick="submitArticle(\'' . $id . '\', \'\', clone' . $this->id_node . ')"'; + $submit_article = ''; + + $admin_buttons = $new_button . $modify_article . $up_button . $down_button . $delete_article . $close_editor . $submit_article; + + // squelette d'un nouvel article + ob_start(); + require self::VIEWS_PATH . 'post.php'; + $new_article = ob_get_clean(); + } + + // articles existants + $this->useChildrenBuilder($node); + $content = $this->html; + + ob_start(); + require $viewFile; + $this->html = ob_get_clean(); // pas de concaténation ici, on écrase + } + } +} \ No newline at end of file diff --git a/src/View/HeadBuilder.php b/src/View/HeadBuilder.php new file mode 100644 index 0000000..4d63457 --- /dev/null +++ b/src/View/HeadBuilder.php @@ -0,0 +1,84 @@ +getName() . '.php'; + + if(file_exists($viewFile)){ + // css et js + $page = Model::$page_path->getLast(); + + $css = ''; + foreach($page->getCSS() as $name){ + $css .= self::insertCSS($name); + } + + $js = ''; + foreach($page->getJS() as $name){ + $js .= self::insertJS($name); + } + + if(MainBuilder::$modif_mode){ + $css .= self::insertCSS('modif_page'); + $js .= self::insertJS('modif_page'); + } + + if(IS_ADMIN){ + // édition éléments sur toutes les pages (header, footer et favicon) + $js .= self::insertJS('Input'); + + // sert partout? + $js .= self::insertJS('Fetcher'); + + // tinymce, nécéssite un script post-install et post-update dans composer.json + $css .= self::insertCSS('tinymce'); + $js .= self::insertJS('tinymce/tinymce.min'); + $js .= self::insertJS('tinymce'); + } + + $title = Model::$page_path->getLast()->getPageName(); + $description = Model::$page_path->getLast()->getDescription(); + + // favicon + // ?-> est l'opérateur de navigation sécurisée => LOVE! + $favicon_name = ($favicon_object = $node->getNodeData()->getAssetByRole('head_favicon'))?->getFileName(); + $favicon = $favicon_name ? Asset::USER_PATH . $favicon_name : ''; + $favicon_type = $favicon_object?->getMimeType() ?? ''; + + ob_start(); + require $viewFile; + $this->html .= ob_get_clean(); + } + } + + static function insertCSS(string $name): string + { + return '' . "\n"; + } + static function insertJS(string $name, bool $module = false): string + { + return '' . "\n"; + } + + static function versionedFileURL(string $type, string $filename): string + { + $path = $type . '/' . $filename . '.' . $type; + + if(file_exists($path)){ + $version = substr(md5_file($path), 0, 8); + return $path . '?v=' . $version; + } + return $path; // sécurité fichier absent + } +} diff --git a/src/View/HeaderBuilder.php b/src/View/HeaderBuilder.php new file mode 100644 index 0000000..14f5ffa --- /dev/null +++ b/src/View/HeaderBuilder.php @@ -0,0 +1,131 @@ +getChildren(); + foreach($children as $child) + { + if($child->getName() === 'nav'){ + //$this->nav = $child; + // actuellement le noeud nav ne contient aucune info utile et l'envoyer à NavBuilder est inutile + $nav_builder = new NavBuilder(/*$this->nav*/); + + $nav = $nav_builder->render(); + } + elseif($child->getName() === 'breadcrumb'){ + //$this->breadcrumb = $child; + $breadcrumb_builder = new BreadcrumbBuilder(/*$this->breadcrumb*/); + $breadcrumb = $breadcrumb_builder->render(); + } + } + + $viewFile = self::VIEWS_PATH . $node->getName() . '.php'; + + if(file_exists($viewFile)) + { + $node_data = $node->getNodeData(); + // titre et description + if(!empty($node_data->getData())) + { + extract($node_data->getData()); + } + + // réseaux sociaux + logo dans l'entête + // ?-> est l'opérateur de chainage optionnel + $header_logo = Asset::USER_PATH . $node_data->getAssetByRole('header_logo')?->getFileName() ?? ''; + $header_background_name = $node_data->getAssetByRole('header_background')?->getFileName(); + $header_background = $header_background_name ? Asset::USER_PATH . $header_background_name : ''; + $social_networks = ''; + + // boutons mode admin + if(IS_ADMIN){ + // assets dans classe header_additional_inputs + $admin_head_favicon = ' + + + '; + $admin_header_background = ' + + + '; + + // asset dans classe header_content + $admin_header_logo = ' + + + '; + + // texte dans classe header_content + $admin_header_title = ' + + + '; + $admin_header_description = ' + + + '; + + // icônes réseaux sociaux + // boucle sur la liste complète de réseaux sociaux + foreach(NodeData::$social_networks as $network){ + $checked = (isset($social_show[$network]) && $social_show[$network]) ? 'checked' : ''; + $href = (isset($social[$network]) && $social[$network] !== '') ? 'href="' . $social[$network] . '"' : ''; + + $social_networks .= '
+ + ' + . $this->insertSVG(self::ICON_PATH . $network . '.svg', ['id' => 'header_' . $network . '_content', 'class' => ($checked ? 'svg_fill_red' : '')]) + . ' + + + + + +
'; + // {'has_content': false} => InputToggle ne gèrera pas cette balise + } + } + else{ + $admin_head_favicon = ''; + $admin_header_background = ''; + $admin_header_logo = ''; + $admin_header_title = ''; + $admin_header_description = ''; + + if(isset($social_show)){ + // boucle sur les réseaux sociaux "activés" + foreach(array_keys($social_show) as $network){ + if($social_show[$network]){ + $href = (isset($social[$network]) && $social[$network] !== '') ? 'href="' . $social[$network] . '"' : ''; + $social_networks .= ''; + } + } + } + } + + ob_start(); + require $viewFile; + $this->html .= ob_get_clean(); + } + } +} \ No newline at end of file diff --git a/src/View/LoginBuilder.php b/src/View/LoginBuilder.php new file mode 100644 index 0000000..3a303b0 --- /dev/null +++ b/src/View/LoginBuilder.php @@ -0,0 +1,56 @@ +getName() . '.php'; + + $captcha = new Captcha; + $_SESSION['captcha'] = $captcha->getSolution(); // enregistrement de la réponse du captcha pour vérification + + //$this->html .= $header; + + $error_messages = [ + 'error_non_valid_captcha' => 'Erreur au test anti-robot, veuillez saisir un nombre entier.', + 'captcha_server_error' => 'captcha_server_error', + 'bad_solution_captcha' => 'Erreur au test anti-robot, veuillez réessayer.', + 'bad_login_or_password' => 'Mauvais identifiant ou mot de passe, veuillez réessayer.', // ne pas indiquer où est l'erreur + 'forbidden_characters' => 'Caractères interdits: espaces, tabulations, sauts CR/LF.' + ]; + + $link_form = new URL(['action' => 'connection']); + isset($_GET['from']) ? $link_form->addParams(['from' => $_GET['from']]) : ''; + isset($_GET['id']) ? $link_form->addParams(['id' => $_GET['id']]) : ''; + + $link_exit = new URL; + isset($_GET['from']) ? $link_exit->addParams(['page' => $_GET['from'] ]) : ''; + isset($_GET['id']) ? $link_exit->addParams(['id' => $_GET['id']]) : ''; + + $error = isset($_GET['error']) ? $error_messages[$_GET['error']] : ''; + + ob_start(); + require $viewFile; + $this->html = ob_get_clean(); // nouveau contenu + + //$this->html .=

Ce site utilise un cookie « obligatoire » lorsque vous êtes connecté ainsi que sur cette page.
Il sera supprimé à votre déconnexion ou dès que vous aurez quitté le site.

; + + //$this->html .= $footer; + } +} diff --git a/src/View/MainBuilder.php b/src/View/MainBuilder.php new file mode 100644 index 0000000..b5b9b00 --- /dev/null +++ b/src/View/MainBuilder.php @@ -0,0 +1,99 @@ +html .= "
\n"; + + // page article: cas particulier où l'article est greffé sur main + if(Model::$page_path->getLast()->getEndOfPath() === 'article'){ + // pas censé arriver + if(!isset($_GET['id'])){ + header('Location: ' . new URL); + die; + } + + // nouvel article + if($node->getAdoptedChild() == null){ + $date = new \DateTime; + $article = new Article('', $date); + $new = new Node('new', 0, null, null, $article); + } + // modification + else{ + $new = $node->getAdoptedChild(); + } + //$builder_name = 'App\View\\' . $this->snakeToPascalCase($new->getName()) . 'Builder'; + $builder_name = 'App\View\NewBuilder'; + $builder = new $builder_name($new); + $this->html .= $builder->render(); + } + else{ + // si action = "modif_page", affiche des commandes supplémentaires + if(IS_ADMIN && self::$modif_mode){ + // ajouter un contrôle du champ in_menu + $this->viewEditBlocks($node); + } + + // dans tous les cas + $this->useChildrenBuilder($node); + } + + $this->html .= "
\n"; + } + + // mode modification de page uniquement + private function viewEditBlocks(Node $node): void + { + $options = ''; + foreach(Presentation::BLOCKS as $key => $value){ + $options .= '\n"; + } + + // ceci pourrait être déplacé au début des blocs + $bloc_edit = ''; + foreach($node->getChildren() as $child_node){ + // ordre des articles 'news' + if($child_node->getName() === 'news_block'){ + $order = $child_node->getNodeData()->getChronoOrder() ? 'chrono' : 'antichrono'; + } + + // présentation par défaut + if(Presentation::hasPresentation($child_node->getName()) && $child_node->getNodeData()->getPresentation() === null){ + $child_node->getNodeData()->setPresentation('full_width'); // pas de persistence ici + } + + ob_start(); + require self::VIEWS_PATH . 'modify_block.php'; + $bloc_edit .= ob_get_clean(); + } + + ob_start(); + require self::VIEWS_PATH . 'modify_page.php'; + $this->html .= ob_get_clean(); + } + + // utilisée dans modify_block.php + private function makePresentationOptions(string $presentation): string + { + $options = ''; + foreach(Presentation::PRESENTATIONS as $key => $value){ + $options .= ''; + } + return $options; + } +} \ No newline at end of file diff --git a/src/View/MaintenanceBuilder.php b/src/View/MaintenanceBuilder.php new file mode 100644 index 0000000..13d391c --- /dev/null +++ b/src/View/MaintenanceBuilder.php @@ -0,0 +1,36 @@ +getName() . '.php'; + + // noter qu'un backup a été créé dans UserController::connect() + try{ + $backup_array = Backup::getBackupList(); + } + catch(\RuntimeException $e){ + $backup_array = []; + $_SESSION['flash_message'] = $e->getMessage(); // peut écraser une flash error déjà dans $_SESSION['flash_message'] + } + + $backup_options = ''; + for($i = count($backup_array) - 1; $i >= 0; $i--){ + $backup_options .= ''; + } + + if(file_exists($viewFile)){ + ob_start(); + require $viewFile; + $this->html = ob_get_clean(); + } + } +} \ No newline at end of file diff --git a/src/View/MenuBuilder.php b/src/View/MenuBuilder.php new file mode 100644 index 0000000..f14cea7 --- /dev/null +++ b/src/View/MenuBuilder.php @@ -0,0 +1,101 @@ +getName() . '.php'; + + if(file_exists($viewFile)) + { + if(IS_ADMIN){ + $this->unfoldMenu(Model::$menu); + + if($template){ + $this->unfoldOptions(Model::$menu); + } + } + else{ + header('Location: ' . new URL); + die; + } + + // si faux, n'utilise pas le template + if($template){ + ob_start(); + require $viewFile; // insertion de $this->html généré par unfoldMenu + $this->html = ob_get_clean(); // pas de concaténation .= cette fois on écrase + } + } + } + + private function unfoldMenu(Page $page): void + { + $this->html .= '
' . "\n"; + + foreach($page->getChildren() as $entry) + { + $checked = $entry->isHidden() ? '' : 'checked'; + $this->html .= '
+ + + + + + + '; + + // entrées URL: bouton poubelle + nom du lien + adresse + if(str_starts_with($entry->getEndOfPath(), 'http')){ + $this->html .= '
+ + +
+ + + + + + + + '; + } + else{ + $this->html .= ' + ' . $entry->getPagePath() . ''; + } + + if(count($entry->getChildren()) > 0){ + $this->unfoldMenu($entry); + } + $this->html .= '
' . "\n"; + } + $this->html .= "
\n"; + } + + private function unfoldOptions(Page $page): void + { + foreach($page->getChildren() as $entry){ + $this->options .= '\n"; + if(count($entry->getChildren()) > 0){ + $this->unfoldOptions($entry); + } + } + } +} \ No newline at end of file diff --git a/src/View/NavBuilder.php b/src/View/NavBuilder.php new file mode 100644 index 0000000..2e3cdbd --- /dev/null +++ b/src/View/NavBuilder.php @@ -0,0 +1,77 @@ +html .= ''; + } + + private function navMainHTML(Page $nav_data, array $current): string + { + $nav_html = ''; + static $level = 0; + + foreach($nav_data->getChildren() as $data) + { + if(!$data->isHidden()){ + $li_class = ''; + if(isset($current[$level]) && $data->getEndOfPath() === $current[$level]->getEndOfPath()){ + $li_class = 'current '; + } + + $link = ''; + if($data->isReachable()) // titre de catégorie du menu non clicable + { + if(str_starts_with($data->getEndOfPath(), 'http')) // lien vers autre site + { + $link .= ''; + } + elseif($data->getEndOfPath() != '') // lien relatif + { + $link .= ''; + } + } + else{ + $link .= ''; + } + + if(count($data->getChildren()) > 0) // titre de catégorie + { + $li_class .= $data->getParent() == null ? 'drop-down' : 'drop-right'; + + $nav_html .= '
  • ' . $link . '

    ' . $data->getPageName() . '

    + +
  • ' . "\n"; + } + else + { + $nav_html .= '
  • ' . $link . '

    ' . $data->getPageName() . '

  • ' . "\n"; + } + } + } + return $nav_html; + } +} \ No newline at end of file diff --git a/src/View/NewBuilder.php b/src/View/NewBuilder.php new file mode 100644 index 0000000..598bb1c --- /dev/null +++ b/src/View/NewBuilder.php @@ -0,0 +1,157 @@ +getName() . '.php'; + + if(file_exists($viewFile)) + { + if(self::$new_article_mode){ + $id = $_GET['id']; // ici l'id est le nom du block news_block parent + $title = ''; + $preview = ''; + + // lettre au début de l'id: t = title, p = preview, i = article, d = date + $id_title = $id; + $id_title[0] = 't'; + $id_preview = $id; + $id_preview[0] = 'p'; + $id_content = 'i' . $id; + $id_content[0] = 'i'; + $id_date = $id; + $id_date[0] = 'd'; + } + else{ + $id = (string)$node->getId(); + + // html, date + $title = $node->getArticle()->getTitle(); + $preview = $node->getArticle()->getPreview(); + + // lettre au début de l'id: t = title, p = preview, i = article, d = date + $id_title = 't' . $id; + $id_preview = 'p' . $id; + $id_content = 'i' . $id; + $id_date = 'd' . $id; + } + + $content = ''; + if(CURRENT_PAGE === 'article'){ + $content = $node->getArticle()->getContent(); + $from_to_button = '

    '; + $overflow = ''; + } + else{ + $from_to_button = '

    '; + $overflow = ' overflow_hidden'; + } + + $date = $node->getArticle()->getDateTime()->format('Y-m-d\TH:i:s.v\Z'); // format: 2025-07-17T13:54:00.000Z + // format(\DateTime::ATOM) produit le format: 2025-10-10T12:17:00+00:00, c'est aussi de la norme ISO, mais à éviter pour être compatible avec date.toISOString en JS + + // partage + $share_link = new URL(['page' => 'article', 'id' => $id]); + $share_js = 'onclick="copyInClipBoard(\'' . $share_link . '\')"'; + if(isset($_GET['id']) && $_GET['id'][0] === 'n'){ + $class = 'class="share hidden"'; + } + else{ + $class = 'class="share"'; + } + $share_button = '

    ' . "\n"; + + // modifier un article + $title_buttons = ''; + $preview_buttons = ''; + $article_buttons = ''; + $date_buttons = ''; + $admin_buttons = ''; + if(IS_ADMIN){ + if(CURRENT_PAGE === 'article'){ + $title_js = 'onclick="openEditor(\'' . $id_title . '\')"'; + $modify_title = '

    ' . "\n"; + $close_js_title = 'onclick="closeEditor(\'' . $id_title . '\')"'; + $close_editor_title = ''; + $submit_js_title = 'onclick="submitArticle(\'' . $id_title . '\')"'; + $submit_title = ''; + $title_buttons = '
    ' . $modify_title . $close_editor_title . $submit_title . '
    '; + + $preview_js = 'onclick="openEditor(\'' . $id_preview . '\')"'; + $modify_preview = '

    ' . "\n"; + $close_js_preview = 'onclick="closeEditor(\'' . $id_preview . '\')"'; + $close_editor_preview = ''; + $submit_js_preview = 'onclick="submitArticle(\'' . $id_preview . '\')"'; + $submit_preview = ''; + $preview_buttons = '
    ' . $modify_preview . $close_editor_preview . $submit_preview . '
    '; + + $article_js = 'onclick="openEditor(\'' . $id_content . '\')"'; + $modify_article = '

    ' . "\n"; + $close_js_article = 'onclick="closeEditor(\'' . $id_content . '\')"'; + $close_editor_article = ''; + $submit_js_article = 'onclick="submitArticle(\'' . $id_content . '\')"'; + $submit_article = ''; + $article_buttons = '
    ' . $modify_article . $close_editor_article . $submit_article . '
    '; + + $date_js = 'onclick="openDatetimeLocalInput(\'' . $id_date . '\', \'article\');'; + $modify_date = '

    ' . "\n"; + $close_js_date = 'onclick="closeInput(\'' . $id_date . '\')"'; + $close_editor_date = ''; + $submit_js_date = 'onclick="submitDate(\'' . $id_date . '\')"'; + $submit_date = ''; + $date_buttons = '
    ' . $modify_date . $close_editor_date . $submit_date . '
    '; + + // mode nouvel article + if(self::$new_article_mode){ + $delete_article = ''; + // valider la création d'un nouvel article + $submit_js = 'onclick="submitArticle(\'' . $_GET['id'] . '\')"'; + $submit_article = '

    ' . "\n"; + } + // mode article existant + else{ + $url = new URL(['action' => 'delete_article', 'id' => $_GET['id'], 'from' => $_GET['from'] ?? '']); + $delete_article = '
    +

    + Supprimer l\'article +

    ' . "\n"; // this.closest('form').submit() = submit du formulaire avec javascript + $submit_article = ''; + } + + $admin_buttons = $share_button . $delete_article . $submit_article . $from_to_button; + } + // autre page + else{ + $delete_js = 'onclick="deleteArticle(\'' . $id . '\')"'; + $delete_article = '

    ' . "\n"; + + $close_editor = ''; + $submit_article = ''; + + $admin_buttons = $from_to_button . $share_button . $delete_article . $close_editor . $submit_article; + } + } + else{ + $admin_buttons = $share_button . $from_to_button; + } + + ob_start(); + require($viewFile); + $this->html .= ob_get_clean(); + } + } +} diff --git a/src/View/NewPageBuilder.php b/src/View/NewPageBuilder.php new file mode 100644 index 0000000..578a098 --- /dev/null +++ b/src/View/NewPageBuilder.php @@ -0,0 +1,53 @@ +getName() . '.php'; + + if(IS_ADMIN && file_exists($viewFile)) + { + /*if(!empty($node->getNodeData()->getData())) + { + extract($node->getNodeData()->getData()); + }*/ + + $this->unfoldOptions(Model::$menu); + + ob_start(); + require $viewFile; // insertion de $this->html généré par unfoldMenu + $this->html = ob_get_clean(); // pas de concaténation .= cette fois on écrase + } + else{ + header('Location: ' . new URL); + die; + } + } + + private function unfoldOptions(Page $page): void + { + foreach($page->getChildren() as $entry){ + $this->options .= '\n"; + if(count($entry->getChildren()) > 0){ + $this->unfoldOptions($entry); + } + } + } +} \ No newline at end of file diff --git a/src/View/NewsBlockBuilder.php b/src/View/NewsBlockBuilder.php new file mode 100644 index 0000000..6b3aba5 --- /dev/null +++ b/src/View/NewsBlockBuilder.php @@ -0,0 +1,76 @@ +getName() . '.php'; // = news_block.php, actuellement identique à post_block.php + + if(file_exists($viewFile)) + { + if(!empty($node->getNodeData()->getData())) + { + extract($node->getNodeData()->getData()); + } + + // stratégie d'affichage du contenu (utilisation de méthodes ou de classe List, GridPresentation, etc) + $section_class = $node->getNodeData()->getPresentation(); // = list, grid , mosaic ou carousel + $cols_min_width = ''; + if($section_class === 'grid'){ + $min_width = $node->getNodeData()->getColsMinWidth(); + $cols_min_width = 'grid-template-columns: repeat(auto-fit, minmax(' . (string)$min_width . 'px, 1fr));'; + } + $fetch_button_hidden = $node->getNodeData()->getNumberOfPages() > 1 ? '' : ' hidden'; + + // ajouter un article + $new_article = ''; + if(IS_ADMIN) + { + $id = 'n' . $this->id_node; + + $share_button = ''; + + $new_button = '

    '; + + $modify_js = 'onclick="openEditor(\'' . $id . '\')"'; + $modify_article = '' . "\n"; + + $delete_js = 'onclick="deleteArticle(\'' . $id . '\')"'; + $delete_article = '' . "\n"; + + $close_js = 'onclick="closeEditor(\'' . $id . '\')"'; + $close_editor = ''; + + $submit_js = 'onclick="submitArticle(\'' . $id . '\', clone' . $this->id_node . ')"'; + $submit_article = ''; + + $html = ''; + $admin_buttons = $new_button . $modify_article . $delete_article . $close_editor . $submit_article; + + // insérer post.php pour le bouton "Nouvel article", new.php pour les vrais articles + ob_start(); + require self::VIEWS_PATH . 'post.php'; // nécéssite $admin_buttons et $html + $new_article = ob_get_clean(); + } + + // articles existants + $this->useChildrenBuilder($node); + $content = $this->html; + + ob_start(); + require $viewFile; // utilise $content + $this->html = ob_get_clean(); // nouveau contenu + } + } +} \ No newline at end of file diff --git a/src/View/PostBlockBuilder.php b/src/View/PostBlockBuilder.php new file mode 100644 index 0000000..850364a --- /dev/null +++ b/src/View/PostBlockBuilder.php @@ -0,0 +1,90 @@ +getName() . '.php'; // = post_block.php, actuellement identique à news_block.php + + if(file_exists($viewFile)) + { + if(!empty($node->getNodeData()->getData())) + { + extract($node->getNodeData()->getData()); + } + + // stratégie d'affichage du contenu (utilisation de méthodes ou de classe List, GridPresentation, etc) + $section_class = $node->getNodeData()->getPresentation(); // = list, grid , mosaic ou carousel + $cols_min_width = ''; + if($section_class === 'grid'){ + $min_width = $node->getNodeData()->getColsMinWidth(); + $cols_min_width = 'grid-template-columns: repeat(auto-fit, minmax(' . (string)$min_width . 'px, 1fr));'; + } + $fetch_button_hidden = $node->getNodeData()->getNumberOfPages() > 1 ? '' : ' hidden'; + + // ajouter un article + // => fait un peu double emploi avec PostBuilder + $new_article = ''; + if(IS_ADMIN) + { + $id = 'n' . $this->id_node; + + $share_button = ''; + + $new_button = '

    ' . "\n" . '

    '; + + $modify_js = 'onclick="openEditor(\'' . $id . '\')"'; + $modify_article = '' . "\n"; + + $up_js = 'onclick="switchPositions(\'' . $id . '\', \'up\')"'; + $up_button = '' . "\n"; + + $down_js = 'onclick="switchPositions(\'' . $id . '\', \'down\')"'; + $down_button = '' . "\n"; + + $delete_js = 'onclick="deleteArticle(\'' . $id . '\')"'; + $delete_article = '' . "\n"; + + $close_js = 'onclick="closeEditor(\'' . $id . '\')"'; + $close_editor = ''; + + $position = ''; + + $submit_js = 'onclick="submitArticle(\'' . $id . '\', clone' . $this->id_node . ')"'; + $submit_article = ''; + + $html = ''; + $admin_buttons = $new_button . $modify_article . $up_button . $down_button . $delete_article . $close_editor . $submit_article . $position; + + // squelette d'un nouvel article + ob_start(); + require self::VIEWS_PATH . 'post.php'; + $new_article = ob_get_clean(); + } + + // articles existants + $this->useChildrenBuilder($node); + $content = $this->html; + + ob_start(); + require $viewFile; + $this->html = ob_get_clean(); // pas de concaténation ici, on écrase + } + } +} \ No newline at end of file diff --git a/src/View/PostBuilder.php b/src/View/PostBuilder.php new file mode 100644 index 0000000..ade7304 --- /dev/null +++ b/src/View/PostBuilder.php @@ -0,0 +1,60 @@ +getName() . '.php'; + + if(file_exists($viewFile)) + { + // html + $title = $node->getArticle()->getTitle(); + $html = $node->getArticle()->getContent(); + $id = (string)$node->getId(); + + // partage + $share_link = new URL(['page' => CURRENT_PAGE], $id); + $share_js = 'onclick="copyInClipBoard(\'' . $share_link . '\')"'; + $share_button = '' . "\n"; + + // modifier un article + // => fait un peu double emploi avec PostBlockBuilder + $admin_buttons = ''; + if(IS_ADMIN) + { + $modify_js = 'onclick="openEditor(\'' . $id . '\')"'; + $modify_article = '

    ' . "\n"; + + $up_js = 'onclick="switchPositions(\'' . $id . '\', \'up\')"'; + $up_button = '

    ' . "\n"; + + $down_js = 'onclick="switchPositions(\'' . $id . '\', \'down\')"'; + $down_button = '

    ' . "\n"; + + $delete_js = 'onclick="deleteArticle(\'' . $id . '\')"'; + $delete_article = '

    ' . "\n"; + + $close_js = 'onclick="closeEditor(\'' . $id . '\')"'; + $close_editor = ''; + + $submit_js = 'onclick="submitArticle(\'' . $id . '\')"'; + $submit_article = ''; + + $admin_buttons = $modify_article . $up_button . $down_button . $delete_article . $close_editor . $submit_article; + } + + ob_start(); + require($viewFile); + $this->html .= ob_get_clean(); + } + } +} diff --git a/src/View/ShowEmailsBuilder.php b/src/View/ShowEmailsBuilder.php new file mode 100644 index 0000000..b02174d --- /dev/null +++ b/src/View/ShowEmailsBuilder.php @@ -0,0 +1,71 @@ +getName() . '.php'; + if(file_exists($viewFile)) + { + // objets Email groupés par destinataire + $emails_by_recipient = []; + foreach($node->getNodeData()->getEmails() as $email){ + $recipient = $email->getRecipient(); + $emails_by_recipient[$recipient][] = $email; + } + + // affiche une table par destinataire + $emails = ''; + foreach($emails_by_recipient as $recipient => $emails_list){ + $html = '

    Destinataire: ' . $recipient . '

    + + + + + + + + + + + + + '; + + // insère les données + foreach($emails_list as $email){ + $html .= ' + + + + + + + + '; + } + + $html .= ' +
    ExpéditeurAdresseContenuDateEffacement prévu leSensible
    ' . htmlspecialchars($email->getSenderName()) . '' . htmlspecialchars($email->getSenderAddress()) . '' . htmlspecialchars($email->getContent()) . '' . $email->getDateTime()->format('d/m/Y') . '' . $email->getDeletionDate()->format('d/m/Y') . 'isSensitive() ? 'checked' : '') . ' onclick="toggleSensitiveEmail(' . $email->getId() . ')">
    '; + $emails .= $html; + } + + ob_start(); + require $viewFile; // insertion de $this->html généré par unfoldMenu + $this->html = ob_get_clean(); // pas de concaténation .= cette fois on écrase + } + else{ + header('Location: ' . new URL(['error' => 'show_emails_view_not_found'])); + die; + } + } +} \ No newline at end of file diff --git a/src/View/UserEditBuilder.php b/src/View/UserEditBuilder.php new file mode 100644 index 0000000..4a9fd90 --- /dev/null +++ b/src/View/UserEditBuilder.php @@ -0,0 +1,64 @@ +getName() . '.php'; + + $error_messages = [ + 'error_non_valid_captcha' => 'Erreur au test anti-robot, veuillez saisir un nombre entier.', + 'captcha_server_error' => 'captcha_server_error', + + 'bad_login_or_password' => 'Mauvais identifiant ou mot de passe, veuillez réessayer.', // ne pas indiquer où est l'erreur + 'bad_solution_captcha' => 'Erreur au test anti-robot, veuillez réessayer.', + 'forbidden_characters' => 'Caractères interdits: espaces, tabulations, sauts CR/LF.', + 'same_username_as_before' => 'Nouveau nom identique au précédent.', + 'same_password_as_before' => 'Nouveau mot de passe identique au précédent.' + ]; + + $error_username = isset($_GET['error_username']) ? $error_messages[$_GET['error_username']] : ''; + $success_username = (isset($_GET['success_username']) && $_GET['success_username']) ? 'Identifiant modifié avec succès.' : ''; + $error_password = isset($_GET['error_password']) ? $error_messages[$_GET['error_password']] : ''; + $success_password = (isset($_GET['success_password']) && $_GET['success_password']) ? 'Mot de passe modifié avec succès.' : ''; + + $captcha = new Captcha; + $_SESSION['captcha'] = $captcha->getSolution(); // enregistrement de la réponse du captcha pour vérification + + $link_user_form = new URL(['action' => 'update_username']); + isset($_GET['from']) ? $link_user_form->addParams(['from' => $_GET['from']]) : ''; + isset($_GET['id']) ? $link_user_form->addParams(['id' => $_GET['id']]) : ''; + + $link_password_form = new URL(['action' => 'update_password']); + isset($_GET['from']) ? $link_password_form->addParams(['from' => $_GET['from']]) : ''; + isset($_GET['id']) ? $link_password_form->addParams(['id' => $_GET['id']]) : ''; + + $link_exit = new URL; + isset($_GET['from']) ? $link_exit->addParams(['page' => $_GET['from'] ]) : ''; + isset($_GET['id']) ? $link_exit->addParams(['id' => $_GET['id']]) : ''; + + $link_logout = new URL(['action' => 'deconnection']); + isset($_GET['from']) ? $link_logout->addParams(['from' => $_GET['from'] ]) : ''; + isset($_GET['id']) ? $link_logout->addParams(['id' => $_GET['id']]) : ''; + + ob_start(); + require $viewFile; + $this->html = ob_get_clean(); // nouveau contenu + } +} \ No newline at end of file diff --git a/src/View/ViewDirector.php b/src/View/ViewDirector.php new file mode 100644 index 0000000..af35179 --- /dev/null +++ b/src/View/ViewDirector.php @@ -0,0 +1,30 @@ +root_node = $root_node; + $this->useChildrenBuilder($this->root_node); + + if(isset($_SESSION['flash_message'])){ + $this->html .= ''; + unset($_SESSION['flash_message']); + } + + return $this->html; + } +} \ No newline at end of file diff --git a/src/View/templates/calendar.php b/src/View/templates/calendar.php new file mode 100644 index 0000000..fd920fd --- /dev/null +++ b/src/View/templates/calendar.php @@ -0,0 +1,14 @@ + +
    + + + +

    +
    +
    + +
    +
    \ No newline at end of file diff --git a/src/View/templates/footer.php b/src/View/templates/footer.php new file mode 100644 index 0000000..cd8bd0a --- /dev/null +++ b/src/View/templates/footer.php @@ -0,0 +1,43 @@ + +
    + +
    +
    + + + +
    + + + + +
    +
    +
    + +
    +
    +
    + + + \ No newline at end of file diff --git a/src/View/templates/form.php b/src/View/templates/form.php new file mode 100644 index 0000000..384bde9 --- /dev/null +++ b/src/View/templates/form.php @@ -0,0 +1,36 @@ + +
    +

    + +
    + + + + + + + + + +
    + +
    + + +
    + +
    + + + + +
    + +
    + +

    +
    +

    + Une copie de votre e-mail (nom, adresse et message) sera conservée dans notre base de données dans le but de pouvoir mieux répondre à votre demande et éventuellement dans d'autres buts (prospection). Ces données seront traitées automatiquement par notre serveur et conservées pendant au maximum mois à compter de votre dernier message. Ce traitement repose sur votre consentement. Vous pouvez consulter, modifier ou supprimer vos données sur simple demande. +

    +
    \ No newline at end of file diff --git a/src/View/templates/form_admin.php b/src/View/templates/form_admin.php new file mode 100644 index 0000000..47bca38 --- /dev/null +++ b/src/View/templates/form_admin.php @@ -0,0 +1,73 @@ + +
    +

    + + onclick="keepEmails(getNodeData()->getId() ?>)"> +

    +

    Notez que ces enregistrements sont des données personnelles et sont concernés par le RGPD.

    +

    +

    + + +

    +

    + + +

    + +
    +
    +

    Paramètres d'envoi

    +

    + + + + +

    +

    + + + +

    +

    + + + + +

    +

    + + + + +

    +

    Il s'agit du service qui acheminera les messages envoyés par ce formulaire.
    + Site web => serveur d'envoi => serveur de réception => votre application "mail"

    +

    + Ce formulaire utilise les paramètres dans le fichier config.ini sur le serveur à mois que vous n'en renseignez d'autres ci-dessus. Les adresses d'envoi et de réception peuvent être identiques. +

    +

    Le site web peut ne pas réussir à se connecter à certains fournisseurs.

    +
    +
    +

    Paramètres de réception

    +

    + + + + +

    +
    +
    +

    +

    Vérifie la connexion au serveur d'envoi. Pour tester la réception, consultez vos e-mails à l'adresse de réception.

    +

    +
    \ No newline at end of file diff --git a/src/View/templates/galery.php b/src/View/templates/galery.php new file mode 100644 index 0000000..97f8cd8 --- /dev/null +++ b/src/View/templates/galery.php @@ -0,0 +1,12 @@ + +
    +

    + + +
    + +
    + +
    \ No newline at end of file diff --git a/src/View/templates/head.php b/src/View/templates/head.php new file mode 100644 index 0000000..bce0a19 --- /dev/null +++ b/src/View/templates/head.php @@ -0,0 +1,13 @@ + + + + + + + <?= $title ?> + + + + + + \ No newline at end of file diff --git a/src/View/templates/header.php b/src/View/templates/header.php new file mode 100644 index 0000000..08bc478 --- /dev/null +++ b/src/View/templates/header.php @@ -0,0 +1,58 @@ + + +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    +
    + +
    +
    +

    + + +

    +

    + + +

    +
    +
    +
    +
    + +
    +
    + +
    +
    + + + +
    \ No newline at end of file diff --git a/src/View/templates/login.php b/src/View/templates/login.php new file mode 100644 index 0000000..3257690 --- /dev/null +++ b/src/View/templates/login.php @@ -0,0 +1,31 @@ + +
    +

    Connexion à l'espace d'administration

    +
    +

    Veuillez saisir votre identifiant et votre mot de passe.

    +

    +
    +

    +

    +

    +

    + + +

    Montrez que vous n'êtes pas un robot.
    + + +

    + + +
    +

    Au fait? Vous n'utilisez pas votre propre ordinateur ou téléphone?
    + Utilisez la navigation privée.

    +
    +
    +
    + +
    \ No newline at end of file diff --git a/src/View/templates/maintenance.php b/src/View/templates/maintenance.php new file mode 100644 index 0000000..5a8a46d --- /dev/null +++ b/src/View/templates/maintenance.php @@ -0,0 +1,65 @@ + +
    +

    Maintenance du site

    +
    +

    +
    + Qui a essayé de se connecter, quand et a-t'il réussi? +

    +
    +

    + +
    +
    +

    + + reçus depuis tous les formulaires de contact +

    +
    +
    +

    + + +
    + Obtenir un fichier SQL à conserver. Une sauvegarde (désignée par "auto") est réalisée à chaque connexion au mode admin. +

    +
    +
    +

    Restaurer la base de données à partir d'un fichier SQL.
    + Attention, l'actuelle BDD sera écrasée! (à l'exception de la table user) +

    +
    +
    + + + +
    +
    +
    + + + +
    +
    +
    +

    + + +
    + Toutes vos photos et vos documents dans un .zip +

    +

    +
    + Gérer les fichiers multimedia, fonction actuellement indisponible +

    +
    + + +
    \ No newline at end of file diff --git a/src/View/templates/menu.php b/src/View/templates/menu.php new file mode 100644 index 0000000..5a15fdb --- /dev/null +++ b/src/View/templates/menu.php @@ -0,0 +1,51 @@ + + +
    + +
    \ No newline at end of file diff --git a/src/View/templates/modify_block.php b/src/View/templates/modify_block.php new file mode 100644 index 0000000..bb5abd3 --- /dev/null +++ b/src/View/templates/modify_block.php @@ -0,0 +1,61 @@ + +
    +
    + +

    + + +

    +
    +

    + + +

    +
    + + + +
    +
    +
    +getName() === 'news_block'){ +?> +
    + + +
    +getName())){ +?> +
    +
    + + +
    +
    + + pixels +
    +
    +
    + +
    + +
    \ No newline at end of file diff --git a/src/View/templates/modify_page.php b/src/View/templates/modify_page.php new file mode 100644 index 0000000..773277b --- /dev/null +++ b/src/View/templates/modify_page.php @@ -0,0 +1,62 @@ + +
    +

    Modification de la page

    +
    +
    +

    + + + +

    +
    + + + + +
    +
    +
    + +
    + + +
    +
    +
    +
    +
    + + + + +
    +
    +
    +
    +

    Ajouter un bloc de page

    +
    +

    +

    +

    + + +

    +
    +
    +

    Articles libres: textes riches générés par l'éditeur et librement positionnables

    +

    Actualités: contenus structurés avec titre, aperçu, date et possédant une page dédiée

    + +
    +
    +
    +

    Modifier un bloc

    + +
    +
    +
    \ No newline at end of file diff --git a/src/View/templates/new.php b/src/View/templates/new.php new file mode 100644 index 0000000..30b244f --- /dev/null +++ b/src/View/templates/new.php @@ -0,0 +1,27 @@ + +
    +
    +
    + +
    + +
    + +
    + +
    + +
    + +
    +

    + + Chargement... +

    +
    + +
    + +
    +
    +
    \ No newline at end of file diff --git a/src/View/templates/new_page.php b/src/View/templates/new_page.php new file mode 100644 index 0000000..535d88d --- /dev/null +++ b/src/View/templates/new_page.php @@ -0,0 +1,38 @@ + +
    +

    Création d'une nouvelle page

    +
    +
    +

    + + +

    +

    + + +

    +

    + + +

    + +

    + + +

    + +
    +
    +
    +
    + +
    \ No newline at end of file diff --git a/src/View/templates/news_block.php b/src/View/templates/news_block.php new file mode 100644 index 0000000..43dbe01 --- /dev/null +++ b/src/View/templates/news_block.php @@ -0,0 +1,14 @@ + +
    +

    + + +
    + +
    +
    + +
    +
    \ No newline at end of file diff --git a/src/View/templates/post.php b/src/View/templates/post.php new file mode 100644 index 0000000..d0d01b2 --- /dev/null +++ b/src/View/templates/post.php @@ -0,0 +1,14 @@ + +
    +
    +
    + +
    +
    + +
    +
    + +
    +
    +
    \ No newline at end of file diff --git a/src/View/templates/post_block.php b/src/View/templates/post_block.php new file mode 100644 index 0000000..43dbe01 --- /dev/null +++ b/src/View/templates/post_block.php @@ -0,0 +1,14 @@ + +
    +

    + + +
    + +
    +
    + +
    +
    \ No newline at end of file diff --git a/src/View/templates/show_emails.php b/src/View/templates/show_emails.php new file mode 100644 index 0000000..f561dc7 --- /dev/null +++ b/src/View/templates/show_emails.php @@ -0,0 +1,30 @@ + +
    +

    Table "email" de la base de données

    +

    + Les e-mails ci-dessous sont des copies de ceux arrivés dans votre boite de messagerie depuis tous les formulaires existant sur le site. Ils sont conservés dans un but pratique (historique, prospection) ou dans tout autre but justifiant leur conservation. Cette conservation est optionnelle et règlementée par la loi européenne RGPD.
    + Notez qu'utiliser cette application ne vous garantit pas d'être en conformité avec la loi, c'est un outil vous permettant de faire les choses comme vous l'entendez. Si tout ceci vous gêne, désactivez le stockage des e-mails et supprimez ceux déjà enregistrés (ci-dessous). +

    +

    Durées de conservation

    +

    + Ce sont des durées maximales, les données peuvent être supprimées plus tôt ou même immédiatement. Le faire est d'ailleurs une obligation dans le cas où les personnes concernées le demandent.
    +

    +

    + Les e-mails ordinaires d'un même expéditeur (même adresse e-mail) sont tous supprimés simultanément lorsque le plus récent d'entre eux atteint les 3 ans (utilisateur est considéré inactif).
    + Les e-mails sensibles quand à eux sont supprimés 5 ans après être devenus sensibles (durée juridique d'une preuve). +

    +

    + Un nettoyeur supprimant les messages dépassant ces durées est exécuté au moment de votre connexion au mode administrateur.
    + Si vos connexions sont rares, il est possible d'automatiser ce nettoyage à l'aide d'une tâche CRON. Pour cela, vous devez configurer le serveur pour qu'il exécute periodiquement la commande "php /chemin/du/site/bin/cron.php". +

    +

    Données sensibles

    +

    + Un e-mail peut-être considéré comme "sensible". Vous pouvez rendre un e-mail sensible lorsqu'il possède une valeur de preuve dans le cas d'un litige.
    + Lorsqu'une personne demande la suppression de ses données personnelles du serveur, les e-mails sensibles peuvent être conservés, vous aurez noté que la durée de conservation est calculée différement. +

    +

    + Les spams ne sont pas sensibles, c'est juste de la pollution, supprimez-les! +

    + + +
    \ No newline at end of file diff --git a/src/View/templates/user_create.php b/src/View/templates/user_create.php new file mode 100644 index 0000000..662d9ac --- /dev/null +++ b/src/View/templates/user_create.php @@ -0,0 +1,54 @@ + 'Erreur au test anti-robot, veuillez saisir un nombre entier.', + 'captcha_server_error' => 'captcha_server_error', + 'bad_solution_captcha' => 'Erreur au test anti-robot, veuillez réessayer.', + 'bad_login_or_password' => 'Mauvais identifiant ou mot de passe, veuillez réessayer.', + 'different_passwords' => 'Les deux mots de passe saisis sont différents', + 'forbidden_characters' => 'Caractères interdits: espaces, tabulations, sauts CR/LF.' +]; +$error = isset($_GET['error']) ? $error_messages[$_GET['error']] : ''; + +$captcha = new Captcha; +$_SESSION['captcha'] = $captcha->getSolution(); // enregistrement de la réponse du captcha pour vérification +?> + + + + + Bienvenue + + + + + +
    +
    +

    Bienvenue.

    +

    Pour commencer, veuillez créer un compte administrateur.

    +
    +

    +
    +

    +

    +

    +

    +

    +

    + + +

    Montrez que vous n'êtes pas un robot.
    + + +

    + + +
    +
    +
    +
    + + \ No newline at end of file diff --git a/src/View/templates/user_edit.php b/src/View/templates/user_edit.php new file mode 100644 index 0000000..9191e7d --- /dev/null +++ b/src/View/templates/user_edit.php @@ -0,0 +1,63 @@ + +
    +
    +
    +

    Mon compte

    +
    + +
    +
    +
    +
    +
    +

    Modifier mon nom d'utilisateur.

    +

    +

    +
    +

    +

    +

    +

    +

    +

    + + +

    Montrez que vous n'êtes pas un robot.
    + + +

    + + +
    +
    +
    +

    Modifier mon mot de passe.

    +

    +

    +
    +

    +

    +

    +

    +

    +

    + + +

    Montrez que vous n'êtes pas un robot.
    + + +

    + + +
    +
    +
    +
    +
    + +
    \ No newline at end of file diff --git a/src/controller/ArticleController.php b/src/controller/ArticleController.php deleted file mode 100644 index 0ddb0e1..0000000 --- a/src/controller/ArticleController.php +++ /dev/null @@ -1,239 +0,0 @@ -query->has('id') && !empty($request->query->get('id')) && $request->query->has('last_article')){ - $id = (int)$request->get('id'); // type et nettoie - $model = new Model($entityManager); - $model->findNodeById($id); - $parent_block = $model->getNode(); - - if(Presentation::hasPresentation($parent_block->getName())){ - $get_articles_return = $model->getNextArticles($parent_block, $request); - $bulk_data = $get_articles_return[0]; - - if($parent_block->getName() === 'post_block'){ - $builder_name = 'PostBuilder'; - } - elseif($parent_block->getName() === 'news_block'){ - $builder_name = 'NewBuilder'; - } - else{ - return new JsonResponse(['success' => false, 'error' => 'server side error'], JsonResponse::HTTP_INTERNAL_SERVER_ERROR); - } - - $html = ''; - foreach($bulk_data as $article){ - $builder = new $builder_name($article); - $html .= $builder->render(); - } - - return new JsonResponse(['success' => true, 'html' => $html, 'truncated' => $get_articles_return[1]]); - } - else{ - return new JsonResponse(['success' => false, 'error' => 'server side error'], JsonResponse::HTTP_INTERNAL_SERVER_ERROR); - } - } - else{ - return new JsonResponse(['success' => false, 'error' => 'bad parameters'], JsonResponse::HTTP_BAD_REQUEST); - } - } - - static public function editorSubmit(EntityManager $entityManager, array $json): JsonResponse - { - if(json_last_error() === JSON_ERROR_NONE){ - $id = $json['id']; - if(in_array($id[0], ['t', 'p', 'i', 'd'])){ - $id = substr($id, 1); - } - - $model = new Model($entityManager); - $content = $json['content']; - - // nettoyage - if(is_array($content)){ // cas d'une nouvelle "news" - foreach($content as $one_input){ - $one_input = Security::secureHTML($one_input); - } - } - else{ // autres cas - $content = Security::secureHTML($json['content']); - } - - // nouvel article - if($json['id'][0] === 'n'){ // ici $id est un bloc - $section_id = (int)substr($id, 1); // id du bloc
    - if(!$model->findNodeById($section_id)){ // erreur mauvais id - return new JsonResponse(['success' => false, 'error' => 'article_not_saved, bad id']); - } - $model->makeSectionNode(); - $section = $model->getNode(); - - // ajout d'une news - if(is_array($content)){ - if($section->getPage()->getEndOfPath() !== $json['from']){ // erreur mauvais from - return new JsonResponse(['success' => false, 'error' => 'article_not_saved, bad from']); - } - - $date = new \DateTime($content['d'] . ':' . (new \DateTime)->format('s')); // l'input type="datetime-local" ne donne pas les secondes, on les ajoute: 'hh:mm' . ':ss' - $article = new Article($content['i'], $date, $content['t'], $content['p']); - $article_node = new Node('new', count($section->getChildren()) + 1, $section, $section->getPage(), $article); - } - // ajout d'un post - else{ - $timestamp = time(); - $date = new \DateTime; - $date->setTimestamp($timestamp); - - $article = new Article($content, $date); - $placement = $json['placement'] === 'first' ? 0 : count($section->getChildren()) + 1; // - $article_node = new Node('post', $placement, $section, $section->getPage(), $article); - - if($json['placement'] === 'first'){ - $section->addChild($article_node); - $section->sortChildren(true); // mettre $article_node au début du tableau puis régénérer les positions (0 devient 1, 1 devient 2...) - } - } - - $entityManager->persist($article_node); - $entityManager->flush(); - - return new JsonResponse(['success' => true, 'article_id' => $article_node->getId()]); - } - // modification article - //else{} - - if($model->makeArticleNode($id)){ // une entrée est trouvée - $node = $model->getArticleNode(); - switch($json['id'][0]){ - case 'i': - $node->getArticle()->setContent($content); - break; - case 'p': - $node->getArticle()->setPreview($content); // html de l'éditeur - break; - case 't': - $node->getArticle()->setTitle($content); // html de l'éditeur - break; - case 'd': - return new JsonResponse(['success' => false, 'message' => 'l\'action editor_submit ne supporte pas les dates, utiliser date_submit.']); - default: // modif article simple (id sans lettre devant) - $node->getArticle()->setContent($content); - } - $entityManager->flush(); - return new JsonResponse(['success' => true]); - } - else{ - return new JsonResponse(['success' => false, 'message' => 'article non identifié']); - } - } - else{ - return new JsonResponse(['success' => false, 'message' => 'Erreur de décodage JSON']); - } - } - - static public function deleteArticle(EntityManager $entityManager, Request $request): Response - { - $model = new Model($entityManager); - - if($request->headers->get('Content-Type') === 'application/json'){ - $id = json_decode($request->getContent(), true)['id']; - } - elseif($request->headers->get('Content-Type') === 'application/x-www-form-urlencoded'){ - $id = $request->query->get('id'); - } - // ni JSON ni form, c'est quoi? un POST vide? - else{ - return new Response('la méthode deleteArticle ne peut être appelée de cette manière'); - } - - if(!$model->makeArticleNode($id, true)){ - $params = ['false', "Erreur 500 pas d\'article à supprimer"]; - } - else{ - $article = $model->getArticleNode(); - $section = $model->getNode(); - - $entityManager->remove($article); - $section->removeChild($article); - $section->sortChildren(true); // régénère les positions - - try{ - $entityManager->flush(); - $params = ['true', 'Article supprimé avec succès']; - } - catch(Exception $e){ - $params = ['false', 'Erreur 500 ' . $e->getMessage()]; - } - } - - if($request->headers->get('Content-Type') === 'application/json'){ - return new JsonResponse( - ['success' => $params[0], 'message' => $params[1]], - $params[0] ? JsonResponse::HTTP_OK : JsonResponse::HTTP_INTERNAL_SERVER_ERROR - ); - } - elseif($request->headers->get('Content-Type') === 'application/x-www-form-urlencoded'){ - $url = new URL(['page' => $request->query->get('from') ?? '', 'success' => $params[0], 'message' => $params[1]]); - return new RedirectResponse((string)$url); - } - else{ - // cas inaccesible - throw new Exception('la méthode deleteArticle ne peut être appelée de cette manière'); - } - } - - static public function switchPositions(EntityManager $entityManager, array $json): JsonResponse - { - $model = new Model($entityManager); - $model->makeArticleNode($json['id1'], true); - $article1 = $model->getArticleNode(); - $section = $model->getNode(); - - $section->sortChildren(true); // régénère les positions avant inversion - $article2 = null; - foreach($section->getChildren() as $child){ - if((string)$child->getId() === $json['id2']) // type string - { - $article2 = $child; - break; - } - } - - // inversion - $tmp = $article1->getPosition(); - $article1->setPosition($article2->getPosition()); - $article2->setPosition($tmp); - $entityManager->flush(); - - return new JsonResponse(['success' => true]); - } - - static public function dateSubmit(EntityManager $entityManager, array $json): JsonResponse - { - $id = substr($json['id'], 1); - $date = new DateTime($json['date']); - - $model = new Model($entityManager); - $model->makeArticleNode($id); - $node = $model->getArticleNode(); - $node->getArticle()->setDateTime($date); - $entityManager->flush(); - - return new JsonResponse(['success' => true]); - } -} \ No newline at end of file diff --git a/src/controller/CalendarController.php b/src/controller/CalendarController.php deleted file mode 100644 index 1a62afa..0000000 --- a/src/controller/CalendarController.php +++ /dev/null @@ -1,74 +0,0 @@ -setTimezone(new DateTimeZone('UTC')); - $end->setTimezone(new DateTimeZone('UTC')); - - // affichage format ISO à l'heure UTC - //$date->format('Y-m-d\TH:i:s\Z'); - - // on prend les évènements se finissant après le début ou commençant avant la fin de la fourchette - $dql = 'SELECT e FROM App\Entity\Event e WHERE e.end >= :start AND e.start <= :end'; - $bulk_data = $entityManager->createQuery($dql) - ->setParameter('start', $start) - ->setParameter('end', $end) - ->getResult(); - - $events = []; - foreach($bulk_data as $one_entry){ - $event = new EventDTO($one_entry); - $events[] = $event->toArray(); - } - - return new JsonResponse($events); - } - - static public function newEvent(array $json, EntityManager $entityManager): JsonResponse - { - try{ - $event = new Event($json); - } - catch(InvalidArgumentException $e){ - return new JsonResponse(['success' => false, 'error' => $e->getMessage()], JsonResponse::HTTP_BAD_REQUEST); // code 400 - } - $entityManager->persist($event); - $entityManager->flush(); - - return new JsonResponse(['success' => true, 'id' => $event->getId()]); - } - static public function updateEvent(array $json, EntityManager $entityManager): JsonResponse - { - $event = $entityManager->find('App\Entity\Event', (int)$json['id']); - try{ - $event->securedUpdateFromJSON($json); - } - catch(InvalidArgumentException $e){ - return new JsonResponse(['success' => false, 'error' => $e->getMessage()], JsonResponse::HTTP_BAD_REQUEST); // code 400 - } - $entityManager->flush(); - - return new JsonResponse(['success' => true]); - } - static public function removeEvent(array $json, EntityManager $entityManager): JsonResponse - { - $event = $entityManager->find('App\Entity\Event', (int)$json['id']); - $entityManager->remove($event); - $entityManager->flush(); - - return new JsonResponse(['success' => true]); - } -} \ No newline at end of file diff --git a/src/controller/ContactFormController.php b/src/controller/ContactFormController.php deleted file mode 100644 index 82e9b17..0000000 --- a/src/controller/ContactFormController.php +++ /dev/null @@ -1,102 +0,0 @@ -find(EmailForm::class, $json['id']); - $form_data->updateData('keep_emails', $json['checked'] ? true : false); - $entityManager->persist($form_data); - $entityManager->flush(); - return new JsonResponse(['success' => true, 'checked' => $json['checked']]); - } - static public function setEmailsRetentionPeriod(EntityManager $entityManager, array $json): JsonResponse - { - $form_data = $entityManager->find(EmailForm::class, $json['id']); - $form_data->updateData($json['field'], (int)$json['months']); - $entityManager->persist($form_data); - $entityManager->flush(); - return new JsonResponse(['success' => true, 'months' => $json['months']]); - } - static public function setEmailParam(EntityManager $entityManager, array $json): JsonResponse - { - $form = new FormValidation($json, ValidationStrategy::EmailParam); - - if($form->validate()){ - $form_data = $entityManager->find(EmailForm::class, $json['id']); - $form_data->updateData($json['what_param'], trim($json['value'])); - $entityManager->persist($form_data); // ?? - $entityManager->flush(); - return new JsonResponse(['success' => true]); - } - else{ - return new JsonResponse(['success' => false, 'error' => $form->getError()]); - } - } - - // les deux méthodes suivantes sont "factorisables", elles ne se distinguent que par la gestion ou non du formulaire rempli par le visiteur - static public function sendVisitorEmail(EntityManager $entityManager, array $json): JsonResponse - { - $form = new FormValidation($json, ValidationStrategy::EmailSend); - - $error = ''; - if($form->validate()){ - // destinataire = e-mail par défaut dans config.ini OU choisi par l'utilisateur - $form_data = $entityManager->find(EmailForm::class, $json['id']); - if($form_data === null){ - return new JsonResponse(['success' => false, 'error' => 'server_error'], JsonResponse::HTTP_INTERNAL_SERVER_ERROR); // code 500 - } - - if(!EmailService::send($entityManager, $form_data, false, $form->getField('name'), $form->getField('email'), $form->getField('message'))){ - $error = 'email_not_sent'; - } - } - else{ - $error = $form->getError(); - } - - if(empty($error)){ - return new JsonResponse(['success' => true]); - } - else{ - return new JsonResponse(['success' => false, 'error' => $error]); - } - } - static public function sendTestEmail(EntityManager $entityManager, array $json): JsonResponse - { - // destinataire = e-mail par défaut dans config.ini OU choisi par l'utilisateur - $form_data = $entityManager->find(EmailForm::class, $json['id']); - if($form_data === null){ - return new JsonResponse(['success' => false, 'error' => 'server_error'], JsonResponse::HTTP_INTERNAL_SERVER_ERROR); - } - - if(EmailService::send($entityManager, $form_data, true, 'nom du visiteur', 'adresse@du_visiteur.fr', "TEST d'un envoi d'e-mail depuis le site web")){ - return new JsonResponse(['success' => true]); - } - else{ - return new JsonResponse(['success' => false, 'error' => 'email_not_sent']); - } - } - static public function deleteEmail(EntityManager $entityManager, array $json): JsonResponse - { - $email = $entityManager->find(Email::class, $json['id']); - $entityManager->remove($email); - $entityManager->flush(); - return new JsonResponse(['success' => true]); - } - static public function toggleSensitiveEmail(EntityManager $entityManager, array $json): JsonResponse - { - $email = $entityManager->find(Email::class, $json['id']); - $email->makeSensitive($json['checked']); - $entityManager->flush(); - return new JsonResponse(['success' => true, 'checked' => $json['checked'], 'deletion_date' => $email->getDeletionDate()->format('d/m/Y')]); - } -} \ No newline at end of file diff --git a/src/controller/FileUploadController.php b/src/controller/FileUploadController.php deleted file mode 100644 index 3cd5e6b..0000000 --- a/src/controller/FileUploadController.php +++ /dev/null @@ -1,65 +0,0 @@ - $file_path]); - } - else{ - return new JsonResponse(['message' => 'Erreur enregistrement du fichier.'], JsonResponse::HTTP_INTERNAL_SERVER_ERROR); // code 500 - } - } - else{ - return new JsonResponse(['message' => 'Erreur 400: fichier non valide.'], JsonResponse::HTTP_BAD_REQUEST); // code 400 - } - } - else{ - return new JsonResponse(['message' => 'Erreur 400: Bad Request'], JsonResponse::HTTP_BAD_REQUEST); // code 400 - } - } -} \ No newline at end of file diff --git a/src/controller/HeadFootController.php b/src/controller/HeadFootController.php deleted file mode 100644 index 72487d8..0000000 --- a/src/controller/HeadFootController.php +++ /dev/null @@ -1,141 +0,0 @@ - false]); - } - - $model = new Model($entityManager); - if($model->findWhateverNode('name_node', $params_array[0])){ - $node_data = $model->getNode()->getNodeData(); - - // liens réseaux sociaux - if(in_array($params_array[1], NodeData::$social_networks)){ - $social = $node_data->getData()['social'] ?? []; - $social[$params_array[1]] = $json['new_text']; - $node_data->updateData('social', $social); - } - // autres textes - else{ - $node_data->updateData($params_array[1], $json['new_text']); // $params_array[1] n'est pas contrôlé - } - - $entityManager->flush(); - return new JsonResponse(['success' => true]); - } - else{ - return new JsonResponse(['success' => false]); - } - } - static public function uploadAsset(EntityManager $entityManager, string $request_params): JsonResponse - { - if(empty($_FILES)){ - return new JsonResponse(['success' => false], JsonResponse::HTTP_BAD_REQUEST); // code 400 - } - else{ - if(!is_dir(Asset::USER_PATH)){ - mkdir(Asset::USER_PATH, 0777, true); - } - - /* -- téléchargement -- */ - $file = $_FILES['file']; - $allowed_extensions = ['jpg', 'jpeg', 'png', 'gif', 'webp', 'tiff', 'tif', 'ico', 'bmp']; // pas de SVG - $extension = strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)); - if(!in_array($extension, $allowed_extensions) || $extension === 'jpg'){ - $extension = 'jpeg'; - } - $mime_type = mime_content_type($file['tmp_name']); - $hash = hash_file('sha256', $file['tmp_name']); - - /* -- instance d'Asset -- */ - $model = new Model($entityManager); - $result = $model->getWhatever('App\Entity\Asset', 'hash', $hash); - - if(count($result) > 0){ // asset existant trouvé - $asset = $result[0]; - - // correction des informations - $name = $asset->getFileName(); // permet à priori de réécrire par dessus le précédent fichier - //$asset->setFileName($name); - $asset->setMimeType($mime_type); - } - else{ - $name = Security::secureFileName(pathinfo($file['name'], PATHINFO_FILENAME)); - $name = uniqid($name . '_') . '.' . $extension; - $asset = new Asset($name, $mime_type, $hash); - } - - /* -- écriture du fichier sur le disque -- */ - if(!ImageUploadController::imagickCleanAndWriteImage(file_get_contents($file['tmp_name']), Asset::USER_PATH . $name)){ // recréer l’image pour la nettoyer - return new JsonResponse(['success' => false, 'message' => "Erreur de l'enregistrement de l'image: problème de permission ou format non valide.", 'format' => $extension], JsonResponse::HTTP_INTERNAL_SERVER_ERROR); // code 500 - } - else{ - $params_array = explode('_', $request_params); // head_favicon, header_logo, header_background, footer_logo - - /* -- table intermédiaire node_data/asset-- */ - if($model->findWhateverNode('name_node', $params_array[0])){ // noeud (head, header ou footer) - $node_data = $model->getNode()->getNodeData(); - - // recherche à l'aide du rôle - $old_nda = null; - foreach($node_data->getNodeDataAssets() as $nda){ - if($nda->getRole() === $request_params){ - $old_nda = $nda; - $old_nda->setAsset($asset); - break; - } - } - // entrée pas trouvée - if($old_nda === null){ - $new_nda = new AssetEmployment($node_data, $asset, $request_params); // $request_params sera le rôle de l'asset - $entityManager->persist($new_nda); - } - - if(count($result) === 0){ - $entityManager->persist($asset); - } - $entityManager->flush(); - return new JsonResponse(['success' => true, 'location' => Asset::USER_PATH . $name, 'mime_type' => $mime_type]); - } - else{ - return new JsonResponse(['success' => false, 'message' => "Erreur noeud non trouvé, c'est pas du tout normal!"], JsonResponse::HTTP_INTERNAL_SERVER_ERROR); // code 500 - } - } - } - } - - static public function displaySocialNetwork(EntityManager $entityManager, string $request_params, array $json): JsonResponse - { - $params_array = explode('_', $request_params); - if(count($params_array) !== 2){ - return new JsonResponse(['success' => false]); - } - - $model = new Model($entityManager); - if(in_array($params_array[1], NodeData::$social_networks) && $model->findWhateverNode('name_node', $params_array[0])){ - $node_data = $model->getNode()->getNodeData(); - $social_show = $node_data->getData()['social_show'] ?? []; - $social_show[$params_array[1]] = $json['checked']; - $node_data->updateData('social_show', $social_show); - - $entityManager->flush(); - return new JsonResponse(['success' => true, 'checked' => $json['checked']]); - } - else{ - return new JsonResponse(['success' => false]); - } - } -} \ No newline at end of file diff --git a/src/controller/ImageUploadController.php b/src/controller/ImageUploadController.php deleted file mode 100644 index af9a553..0000000 --- a/src/controller/ImageUploadController.php +++ /dev/null @@ -1,191 +0,0 @@ -readImageBlob($image_data); - $imagick->stripImage(); // nettoyage métadonnées - //$imagick->setImageFormat($format); // inutile, writeImage force la conversion - - // compression - switch($format){ - case 'jpeg': // particularité du switch, si 'jpeg' le test de 'jpg' est ignoré et on va jusqu'au break - case 'jpg': - $imagick->setImageCompression(Imagick::COMPRESSION_JPEG); - $imagick->setImageCompressionQuality(85); - break; - case 'webp': - $imagick->setImageCompression(Imagick::COMPRESSION_WEBP); - $imagick->setImageCompressionQuality(85); - break; - case 'png': - $imagick->setImageCompression(Imagick::COMPRESSION_ZIP); - $imagick->setImageCompressionQuality(7); // 9 est sans perte - break; - case 'tiff': - $imagick->setImageCompression(Imagick::COMPRESSION_LZW); // LZW est sans perte - break; - } - - // enregistrement - // writeImage utilise l'extension du fichier et ignore le format détecté - // imagemagick est à l'origine une appli console, elle considère que l'extension montre l'intention de l'utilisateur - $imagick->writeImage($local_path); // enregistrement - $imagick->clear(); - $imagick->destroy(); - return true; - } - catch(Exception $e){ - return false; - } - } - static public function curlDownloadImage(string $url, int $maxRetries = 3, int $timeout = 10): string|false - { - $attempt = 0; - $imageData = false; - - while($attempt < $maxRetries){ - $ch = curl_init($url); // instance de CurlHandle - curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); - curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); - curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $timeout); - curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); - curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); - curl_setopt($ch, CURLOPT_USERAGENT, 'TinyMCE-Image-Downloader'); - - $imageData = curl_exec($ch); - $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); - //$curlError = curl_error($ch); - - if($imageData !== false && $httpCode >= 200 && $httpCode < 300){ - return $imageData; - } - - $attempt++; - sleep(1); - } - - return false; // échec après trois tentatives - } - - // téléchargement par le plugin (bouton "insérer une image") - static public function imageUploadTinyMce(): JsonResponse - { - if(!isset($_FILES['file'])){ - return new JsonResponse(['message' => 'Erreur 400: Bad Request'], JsonResponse::HTTP_BAD_REQUEST); // code 400 - } - if(!is_uploaded_file($_FILES['file']['tmp_name'])) { - return new JsonResponse(['message' => "Le fichier n'a pas été téléchargé correctement."], JsonResponse::HTTP_BAD_REQUEST); // code 400 - } - - $dest = 'user_data/images/'; - $dest_mini = 'user_data/images-mini/'; - - // Vérifier si les répertoires existent, sinon les créer - if(!is_dir($dest)){ - mkdir($dest, 0755, true); - } - if(!is_dir($dest_mini)){ - mkdir($dest_mini, 0755, true); - } - - $name = Security::secureFileName(pathinfo($_FILES['file']['name'], PATHINFO_FILENAME)); - $extension = strtolower(pathinfo($_FILES['file']['name'], PATHINFO_EXTENSION)); - $image_data = file_get_contents($_FILES['file']['tmp_name']); - if(!in_array($extension, self::ALLOWED_EXTENSIONS)){ - $extension = 'jpeg'; - } - $local_path = uniqid($dest . $name . '_') . '.' . $extension; - - // créer une miniature de l'image - // - - if(self::imagickCleanAndWriteImage($image_data, $local_path)){ // recréer l’image pour la nettoyer - return new JsonResponse(['location' => $local_path]); // renvoyer l'URL de l'image téléchargée - } - else{ - return new JsonResponse(['message' => 'Erreur image non valide'], JsonResponse::HTTP_INTERNAL_SERVER_ERROR); // code 500 - } - } - - // collage de HTML => recherche de balises , 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 deleted file mode 100644 index 5b67fa8..0000000 --- a/src/controller/MaintenanceController.php +++ /dev/null @@ -1,147 +0,0 @@ -getRepository(Log::class)->findAll(); - if(empty($data)){ - return new JsonResponse(['success' => false]); - } - else{ - $view = '

    Table ' . TABLE_PREFIX . 'log de la base de données

    - - - - - - - - '; - foreach($data as $entry){ - $view .= ' - - - '; - } - $view .= '
    date et heure (Greenwich)connexion réussie
    ' . $entry->getFormatedDate() . '' . ($entry->getSuccess() ? 'oui' : 'non') . '
    '; - 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/' . UserDataService::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 deleted file mode 100644 index 9bd6ad1..0000000 --- a/src/controller/MenuAndPathsController.php +++ /dev/null @@ -1,219 +0,0 @@ -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 deleted file mode 100644 index 11aab09..0000000 --- a/src/controller/PageManagementController.php +++ /dev/null @@ -1,362 +0,0 @@ -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 deleted file mode 100644 index b68421f..0000000 --- a/src/controller/UserController.php +++ /dev/null @@ -1,230 +0,0 @@ - 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); - -use Doctrine\ORM\EntityManager; -use App\Entity\User; -use App\Entity\Log; -use Symfony\Component\HttpFoundation\RedirectResponse; - -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 deleted file mode 100644 index d1aaee7..0000000 --- a/src/controller/ViewController.php +++ /dev/null @@ -1,63 +0,0 @@ -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 deleted file mode 100644 index 4086d5a..0000000 --- a/src/model/DatabaseSettingUp.php +++ /dev/null @@ -1,155 +0,0 @@ -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/EventDTO.php b/src/model/EventDTO.php deleted file mode 100644 index 6dd3722..0000000 --- a/src/model/EventDTO.php +++ /dev/null @@ -1,51 +0,0 @@ -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 deleted file mode 100644 index d277259..0000000 --- a/src/model/Menu.php +++ /dev/null @@ -1,42 +0,0 @@ -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 deleted file mode 100644 index 15ff12a..0000000 --- a/src/model/Model.php +++ /dev/null @@ -1,312 +0,0 @@ -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
    à la création d'un article - public function makeSectionNode(): bool - { - $bulk_data = $this->entityManager - ->createQuery('SELECT n FROM App\Entity\Node n WHERE n.parent = :parent') - ->setParameter('parent', $this->node) - ->getResult(); - - foreach($bulk_data as $article){ - $this->node->addChild($article); // pas de flush, on ne va pas écrire dans la BDD à chaque nouvelle page - } - return true; - } - - public function findUniqueNodeByName(string $name): void // = unique en BDD, donc sans "page" associée - { - $bulk_data = $this->entityManager - ->createQuery('SELECT n FROM App\Entity\Node n WHERE n.name_node = :name') - ->setParameter('name', $name) - ->getResult(); - $this->node = $bulk_data[0]; - } - - public function findItsChildren(): void - { - $bulk_data = $this->entityManager - ->createQuery('SELECT n FROM App\Entity\Node n WHERE n.parent = :parent AND n.page = :page') - ->setParameter('parent', $this->node) - ->setParameter('page', $this->page) - ->getResult(); - foreach($bulk_data as $child){ - $this->node->addChild($child); - } - } - - private function getAllEmails(): array - { - $dql = 'SELECT e FROM App\Entity\Email e'; - return $this->entityManager - ->createQuery($dql) - ->getResult(); - } - /*private function getFormsNodeData(): array - { - $dql = 'SELECT fd FROM App\Entity\NodeData fd WHERE node.name_node = form'; - return $this->entityManager - ->createQuery($dql) - //->setParameter('page', $this->page) - ->getResult(); - }*/ - //private function getEmailsBySender(string $sender): array -} diff --git a/src/model/Path.php b/src/model/Path.php deleted file mode 100644 index 4303952..0000000 --- a/src/model/Path.php +++ /dev/null @@ -1,76 +0,0 @@ -findPage(Model::$menu, $path_array); // remplit $this->current_page - } - catch(Exception $e){} - } - - // produit un tableau de Page en comparant le chemin demandé avec les données dans Menu - // succès => une exception est lancée pour sortir des fonctions imbriquées - // echec => redirection vers la page erreur 404 - private function findPage(Page|Menu $menu, array $path_array): void - { - // recherche dans les autres pages - if($menu instanceof Menu){ - foreach($menu->getOtherPages() as $page){ - if($path_array[0] === $page->getEndOfPath()){ - $this->current_page[] = $page; - throw new Exception(); - } - } - } - // recherche dans le menu - foreach($menu->getChildren() as $page){ - if($path_array[0] === $page->getEndOfPath()){ - $this->current_page[] = $page; - if(count($path_array) > 1){ - array_shift($path_array); // $this->path_array n'est pas modifié, un tableau PHP est passé à une fonction par copie - $this->findPage($page, $path_array); - } - else{ - throw new Exception(); // sortir de tous les findPage() en même temps - } - } - } - - // rien trouvé - header('Location: '. new URL(['page' => Model::$menu->getChildren()[0]->getEndOfPath()])); - die; - } - - public function getString(): string - { - $path_string = ""; - foreach($this->current_page as $one_page){ - $path_string .= $one_page->getEndOfPath() . '/'; - } - return rtrim($path_string, '/'); - } - public function getArray(): array - { - return $this->current_page; - } - - // c'est là qu'on est quoi - public function getLast(): Page - { - return $this->current_page[count($this->current_page) - 1]; - } -} \ No newline at end of file diff --git a/src/model/Position.php b/src/model/Position.php deleted file mode 100644 index fdba271..0000000 --- a/src/model/Position.php +++ /dev/null @@ -1,50 +0,0 @@ -children); $i++) - { - $tmp = $this->children[$i]; - $j = $i - 1; - - // Déplacez les éléments du tableau qui sont plus grands que la clé à une position devant leur position actuelle - while($j >= 0 && $this->children[$j]->getPosition() > $tmp->getPosition()) { - $this->children[$j + 1] = $this->children[$j]; - $j--; - } - $this->children[$j + 1] = $tmp; - } - - // récursivité (utile dans Menu et chemin) - // appel dans Page et dans MenuAndPathController (un tri supplémentaire à faire?) - if(self::class === 'App\Entity\Page'){ - foreach($this->children as $child) { - if(count($child->children) > 0) { - $child->sortChildren($reindexation); - } - } - } - - if($reindexation){ - $this->reindexPositions(); - } - } - - // nouvelles positions (tableau $children => BDD) - public function reindexPositions(): void - { - $i = 1; - foreach($this->children as $child){ - $child->setPosition($i); - $i++; - } - } -} \ No newline at end of file diff --git a/src/model/doctrine-bootstrap.php b/src/model/doctrine-bootstrap.php deleted file mode 100644 index 127f72f..0000000 --- a/src/model/doctrine-bootstrap.php +++ /dev/null @@ -1,33 +0,0 @@ - Config::$db_driver, - 'user' => Config::$user, - 'password' => Config::$password, - 'host' => Config::$db_host, - 'dbname' => Config::$database, -], $config); - -// obtaining the entity manager -$entityManager = new EntityManager($connection, $config); - -foreach($entityManager->getMetadataFactory()->getAllMetadata() as $class){} \ No newline at end of file diff --git a/src/model/entities/AppMetadata.php b/src/model/entities/AppMetadata.php deleted file mode 100644 index ae42ad9..0000000 --- a/src/model/entities/AppMetadata.php +++ /dev/null @@ -1,46 +0,0 @@ -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/entities/Article.php b/src/model/entities/Article.php deleted file mode 100644 index b9cb4bb..0000000 --- a/src/model/entities/Article.php +++ /dev/null @@ -1,94 +0,0 @@ - '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/entities/Asset.php b/src/model/entities/Asset.php deleted file mode 100644 index d76ba08..0000000 --- a/src/model/entities/Asset.php +++ /dev/null @@ -1,68 +0,0 @@ -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/entities/AssetEmployment.php b/src/model/entities/AssetEmployment.php deleted file mode 100644 index 04bbd9a..0000000 --- a/src/model/entities/AssetEmployment.php +++ /dev/null @@ -1,54 +0,0 @@ - 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/entities/Email.php b/src/model/entities/Email.php deleted file mode 100644 index 60fcc1f..0000000 --- a/src/model/entities/Email.php +++ /dev/null @@ -1,131 +0,0 @@ - '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/entities/EmailForm.php b/src/model/entities/EmailForm.php deleted file mode 100644 index 0dcdeec..0000000 --- a/src/model/entities/EmailForm.php +++ /dev/null @@ -1,57 +0,0 @@ -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/entities/Event.php b/src/model/entities/Event.php deleted file mode 100644 index ae0d396..0000000 --- a/src/model/entities/Event.php +++ /dev/null @@ -1,129 +0,0 @@ - 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/entities/Image.php b/src/model/entities/Image.php deleted file mode 100644 index 2ee379a..0000000 --- a/src/model/entities/Image.php +++ /dev/null @@ -1,89 +0,0 @@ - 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 "Image"; - } else { - echo "Image non trouvée."; - } - }*/ -} diff --git a/src/model/entities/Log.php b/src/model/entities/Log.php deleted file mode 100644 index 222b8db..0000000 --- a/src/model/entities/Log.php +++ /dev/null @@ -1,38 +0,0 @@ - '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/entities/Node.php b/src/model/entities/Node.php deleted file mode 100644 index 7932efc..0000000 --- a/src/model/entities/Node.php +++ /dev/null @@ -1,166 +0,0 @@ -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/entities/NodeData.php b/src/model/entities/NodeData.php deleted file mode 100644 index 1646c92..0000000 --- a/src/model/entities/NodeData.php +++ /dev/null @@ -1,102 +0,0 @@ - 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/entities/Page.php b/src/model/entities/Page.php deleted file mode 100644 index 8d3dd40..0000000 --- a/src/model/entities/Page.php +++ /dev/null @@ -1,221 +0,0 @@ -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/entities/Presentation.php b/src/model/entities/Presentation.php deleted file mode 100644 index d66bf24..0000000 --- a/src/model/entities/Presentation.php +++ /dev/null @@ -1,135 +0,0 @@ -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/entities/User.php b/src/model/entities/User.php deleted file mode 100644 index 36bc4db..0000000 --- a/src/model/entities/User.php +++ /dev/null @@ -1,67 +0,0 @@ -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/service/Backup.php b/src/service/Backup.php deleted file mode 100644 index 2ee69c1..0000000 --- a/src/service/Backup.php +++ /dev/null @@ -1,233 +0,0 @@ -format('Y-m-d') . '_' . $type . '.sql'; - - // les versions de mysql sont comme ci: 8.0.36 - // celles de mariadb sont comme ça: 10.11.6-MariaDB - $version = $entityManager->getConnection()->fetchOne('SELECT VERSION()'); - $engine = stripos($version, 'mariadb') !== false ? 'mariadb-dump' : 'mysqldump'; - - $tmp = tempnam('../var', 'tmp_db_codes_'); // crée un fichier avec un nom aléatoire et des droits 600 (concurrence) - // attention, si le dossier de destination n'est pas disponible, le fichier est placé avec les fichiers temporaires - file_put_contents($tmp, - "[client]\n - user=" . Config::$user . "\n - password=" . Config::$password . "\n - host=" . Config::$db_host . "\n"); - - $command = new Process([ - $engine, - '--defaults-extra-file=' . $tmp, // pour ne pas enregistrer les codes dans l'historique de la console ou dans les processus de l'OS - '--single-transaction', - '--quick', // évite d'exploser la RAM si beaucoup de données - '--result-file=' . $file_path, - Config::$database - ]); - - try{ - // unlink et chmod permettent que le serveur et l'utilisateur appelant bin/mysqldump.php réussissent - if(file_exists($file_path)){ - unlink($file_path); - } - $command->mustRun(); // comme run() mais lance une ProcessFailedException - - //$file_path = self::gzipCompress($file_path); // '.gz' ajouté à la fin - - chmod($file_path, 0666); - return $file_path; - } - finally{ - // exécuté même quand situé après "return" - unlink($tmp); - self::cleanBackups(); - } - } - - // compression gzip (gros gain de place sur le serveur), nécessite l'extension zlib - static public function gzipCompress(string $file_path): string - { - try{ - file_put_contents( - $file_path . '.gz', - gzencode(file_get_contents($file_path), 5), // plus rapide que 9 et taille identique d'après mes essais - ); - unlink($file_path); - $file_path .= '.gz'; - } - finally{ - return $file_path; - } - } - - static public function getBackupList(): array - { - $files = scandir(Backup::$backup_dir); // affiche un warning si échoue (à cacher en prod) - if(!$files){ - throw new RuntimeException("Le serveur a rencontré une erreur:
    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; - } -} \ No newline at end of file diff --git a/src/service/Captcha.php b/src/service/Captcha.php deleted file mode 100644 index d57f912..0000000 --- a/src/service/Captcha.php +++ /dev/null @@ -1,55 +0,0 @@ - on pourrait appliquer le pattern "singleton" (justification = le captcha devient une sorte de ressource partagée) - -declare(strict_types=1); - -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 deleted file mode 100644 index 7077e01..0000000 --- a/src/service/Config.php +++ /dev/null @@ -1,88 +0,0 @@ -Le fichier config/config.ini n'existe pas ou n'est pas lisible.

    "; - } - define('TABLE_PREFIX', self::$table_prefix); - } - - // renseigner les variables internes de Config - static private function hydrate(array $raw_data): void - { - foreach($raw_data as $field => $value) - { - if($value != '') // valeur par défaut - { - if(isset(self::$$field)) // le champ existe dans Config - { - // problème du slash à la fin du nom d'un dossier - $value = self::slashAtEndOfPath($field, $value); - self::$$field = $value; - } - else - { - echo "debug: le fichier config.ini comporte une erreur, le champ: " . $field . " est incorrect,\nl'information contenue sur cette ligne ne sera pas utilisée\n"; - } - } - /*else - { - echo "debug: le champ " . $field . " est vide, la valeur par défaut " . self::$$field . " sera utilisée.\n"; - }*/ - } - } - - - // pour que les chemins finissent toujours par un / - static private function slashAtEndOfPath(string $field, string $value): string - { - foreach(self::$path_vars as $item) - { - if($field === $item){ - return !str_ends_with($value, '/') ? $value . '/' : $value; - } - } - return $value; - } -} diff --git a/src/service/EmailService.php b/src/service/EmailService.php deleted file mode 100644 index 1579bdb..0000000 --- a/src/service/EmailService.php +++ /dev/null @@ -1,104 +0,0 @@ - exceptions - $mail->CharSet = 'UTF-8'; - - $smtp_host = $form_data->getData()['smtp_host'] ?? Config::$smtp_host; - $smtp_secure = $form_data->getData()['smtp_secure'] ?? Config::$smtp_secure; - $smtp_username = $form_data->getData()['smtp_username'] ?? Config::$smtp_username; - $smtp_password = $form_data->getData()['smtp_password'] ?? Config::$smtp_password; - - $email_from = $form_data->getData()['email_from'] ?? Config::$email_from; // adresse de l'expéditeur affichée - $email_from = empty($email_from) ? $smtp_username : $email_from; // si vide - $email_from_name = $form_data->getData()['email_from_name'] ?? Config::$email_from_name; // = nom de l'expéditeur affiché - $email_dest = $form_data->getData()['email_dest'] ?? Config::$email_dest; - $email_dest_name = $form_data->getData()['email_dest_name'] ?? Config::$email_dest_name; // = destinataire formulaire - - try{ - // Paramètres du serveur - $mail->isSMTP(); - $mail->Host = $smtp_host; - $mail->SMTPAuth = true; - $mail->Port = 25; - - if($mail->SMTPAuth){ - $mail->Username = $smtp_username; // e-mail - $mail->Password = $smtp_password; - $mail->SMTPSecure = $smtp_secure; // tls (starttls) ou ssl (smtps) - if($mail->SMTPSecure === 'tls'){ - $mail->Port = 587; - } - elseif($mail->SMTPSecure === 'ssl'){ - $mail->Port = 465; - } - } - //var_dump($mail->smtpConnect());die; // test de connexion - - // Expéditeur et destinataire - // $email_from, $email_from_name et $email_dest_name sont modifiables uniquement dans le config.ini pour l'instant - $mail->setFrom(strtolower($email_from), $email_from_name); - $mail->addAddress(strtolower($email_dest), $email_dest_name); - - // Contenu - $mail->isHTML(true); - if($test_email){ - $mail->Subject = "TEST d'un envoi d'e-mail depuis le site web"; - } - else{ - $mail->Subject = 'Message envoyé par: ' . $name . ' (' . $email . ') depuis le site web'; - } - $mail->Body = $message; - $mail->AltBody = $message; - - $mail->send(); - - // copie en BDD - if(!$test_email && ($form_data->getData()['keep_emails'] ?? self::KEEP_EMAILS_DEFAULT)){ - $db_email = new Email($name, $email, $email_dest, $message, $form_data); - $entityManager->persist($db_email); - self::updateLastContactDate($entityManager, $email); - $entityManager->flush(); - } - - return true; - } - catch(Exception $e){ - echo "Le message n'a pas pu être envoyé. Erreur : {$e}
    {$mail->ErrorInfo}"; - return false; - } - } - - static public function updateLastContactDate(EntityManager $entityManager, string $sender): void - { - foreach($entityManager->getRepository('App\Entity\Email')->findAll() as $email){ - $email->getSenderAddress() === $sender ? $email->updateLastContactDate() : null; - } - } - - // peut être appelée par bin/clean_emails_cron.php - static public function cleanEmails(EntityManager $entityManager): void - { - $emails = $entityManager->getRepository('App\Entity\Email')->findAll(); - foreach($emails as $email){ - if($email->getDeletionDate() < new \DateTime()){ - $entityManager->remove($email); - } - } - $entityManager->flush(); - } -} \ No newline at end of file diff --git a/src/service/FormValidation.php b/src/service/FormValidation.php deleted file mode 100644 index 9d0b838..0000000 --- a/src/service/FormValidation.php +++ /dev/null @@ -1,214 +0,0 @@ - 'emailSendStrategy', - self::EmailParam => 'emailParamStrategy', - self::CreateUser => 'createUserStrategy', - self::Connection => 'connectionStrategy', - self::UsernameUpdate => 'usernameUpdateStrategy', - self::PasswordUpdate => 'passwordUpdateStrategy' - }; - } -} - -class FormValidation -{ - private array $data; // tableau associatif (probablement $_POST) - private Closure $callStrategy; - private ?string $error; - - public function __construct(array $data, ValidationStrategy $strategy){ - $this->data = $data; - $method = $strategy->method(); - $this->callStrategy = fn() => $this->$method(); - } - - public function validate(): bool - { - $this->error = ($this->callStrategy)(); // appel de la callback - return empty($this->errors); - } - - public function getError(): string - { - return $this->error; - } - - public function getField(string $field): string - { - return $this->data[$field] ?? ''; - } - - // méthodes de validation - private function captchaValidate(bool $clean_session = true): ?string - { - $captcha_solution = (isset($_SESSION['captcha']) && is_int($_SESSION['captcha'])) ? $_SESSION['captcha'] : 0; - $captcha_try = isset($this->data['captcha']) ? Captcha::controlInput($this->data['captcha']) : 0; - if($clean_session){ - unset($_SESSION['captcha']); - } - - if($captcha_try == 0){ - return 'error_non_valid_captcha'; - } - elseif($captcha_solution == 0){ // ne peut pas arriver, si? - return 'captcha_server_error'; - } - elseif($captcha_try !== $captcha_solution){ - return 'bad_solution_captcha'; - } - return null; - } - - // 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)); - } - - - // stratégies - private function emailSendStrategy(): ?string - { - if($error = $this->captchaValidate(false)){ - return $error; - } - - if(empty($this->data['name']) || empty($this->data['email']) || empty($this->data['message']) - || !isset($this->data['hidden']) || !empty($this->data['hidden'])){ - return 'missing_fields'; - } - - elseif(!filter_var(trim($this->data['email']), FILTER_VALIDATE_EMAIL)){ - return 'bad_email_address'; - } - - $this->data['name'] = htmlspecialchars(trim($this->data['name'])); - $this->data['email'] = htmlspecialchars(trim($this->data['email'])); - $this->data['message'] = htmlspecialchars($this->data['message']); - - return null; - } - private function emailParamStrategy(): ?string - { - if(empty($this->data['id']) || empty($this->data['what_param']) || empty($this->data['value']) - || !isset($this->data['hidden']) || !empty($this->data['hidden'])){ - return 'missing_fields'; - } - - if(!in_array($this->data['what_param'], ['smtp_host', 'smtp_secure', 'smtp_username', 'smtp_password', 'email_dest'])){ - return 'unknown_parameter'; - } - elseif($this->data['what_param'] === 'smtp_username' || $this->data['what_param'] === 'email_dest'){ - if(!filter_var($this->data['value'], FILTER_VALIDATE_EMAIL)){ - return 'invalide_email_address'; - } - } - return null; - } - private function createUserStrategy(): ?string - { - if($error = $this->captchaValidate()){ - return $error; - } - - // test mauvais paramètres - if(empty($this->data['login']) || empty($this->data['password']) || empty($this->data['password_confirmation']) - || !isset($this->data['create_user_hidden']) || !empty($this->data['create_user_hidden'])) - { - return 'bad_login_or_password'; - } - - if($this->data['password'] !== $this->data['password_confirmation']){ - return 'different_passwords'; - } - - if($this->data['login'] !== self::removeSpacesTabsCRLF(htmlspecialchars($this->data['login'])) - || $this->data['password'] !== self::removeSpacesTabsCRLF(htmlspecialchars($this->data['password']))){ - return 'forbidden_characters'; - } - return null; - } - private function connectionStrategy(): ?string - { - if($error = $this->captchaValidate()){ - return $error; - } - - if(empty($this->data['login']) || empty($this->data['password']) - || !isset($this->data['connection_hidden']) || !empty($this->data['connection_hidden'])) - { - return 'bad_login_or_password'; - } - return null; - } - private function usernameUpdateStrategy(): ?string - { - if($error = $this->captchaValidate()){ - return $error; - } - - if(empty($this->data['login']) || empty($this->data['password']) || empty($this->data['new_login']) - || !isset($this->data['modify_username_hidden']) || !empty($this->data['modify_username_hidden'])) - { - return 'bad_login_or_password'; - } - - $new_login = self::removeSpacesTabsCRLF(htmlspecialchars($this->data['new_login'])); - if($new_login !== $this->data['new_login']){ - return 'forbidden_characters'; - } - - if($this->data['login'] !== $_SESSION['user']['username']){ - return 'bad_login_or_password'; - } - if($this->data['login'] === $new_login){ - return 'same_username_as_before'; - } - return null; - } - private function passwordUpdateStrategy(): ?string - { - if($error = $this->captchaValidate()){ - return $error; - } - - if(empty($this->data['login']) || empty($this->data['password']) || empty($this->data['new_password']) - || !isset($this->data['modify_password_hidden']) || !empty($this->data['modify_password_hidden'])) - { - return 'bad_login_or_password'; - } - - $new_password = self::removeSpacesTabsCRLF(htmlspecialchars($this->data['new_password'])); - if($new_password !== $this->data['new_password']){ - return 'forbidden_characters'; - } - - if($this->data['login'] !== $_SESSION['user']['username']){ - return 'bad_login_or_password'; - } - if($this->data['password'] === $new_password){ - return 'same_password_as_before'; - } - return null; - } -} \ No newline at end of file diff --git a/src/service/Installation.php b/src/service/Installation.php deleted file mode 100644 index f7de2cf..0000000 --- a/src/service/Installation.php +++ /dev/null @@ -1,107 +0,0 @@ -L'extension " . $extension . " est manquante.

    "); - $flag = true; - } - } - if(!class_exists(DOMDocument::class)){ // théoriquement plus fiable que extension_loaded() - echo("

    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.

    '; - die; - } - } - - static public function checkFilesAndFoldersRights(): void - { - // -- droits des fichiers et dossiers -- - $droits_dossiers = 0777; - - $flag = false; - if(!file_exists('user_data')){ - try{ - mkdir('user_data/'); - chmod('user_data/', $droits_dossiers); - } - catch(Exception $e){ - echo '

    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

    '; - echo $e; - $flag = true; - } - } - if(!file_exists('../var')){ - try{ - mkdir('../var'); - chmod('../var', $droits_dossiers); - } - catch(Exception $e){ - echo $e; - $flag = true; - } - } - if(!file_exists('../var/backups')){ - try{ - mkdir('../var/backups'); - chmod('../var/backups', $droits_dossiers); // autoriser à la fois le serveur et les scripts dans bin/ - } - catch(Exception $e){ - echo $e; - $flag = true; - } - } - - // droits 600 pour celui-ci - if(!file_exists('../config/config.ini')){ - // aide à la création du config.ini - echo '

    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 deleted file mode 100644 index 28a6c39..0000000 --- a/src/service/Router.php +++ /dev/null @@ -1,376 +0,0 @@ - 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); - -use Symfony\Component\HttpFoundation\Request; -use Doctrine\ORM\EntityManager; -use Symfony\Component\HttpFoundation\JsonResponse; -use Symfony\Component\HttpFoundation\RedirectResponse; -use Symfony\Component\HttpFoundation\Response; - -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 - case 'upload_image_url': - return ImageUploadController::uploadImageHtml(); - // collage d'une image (code base64 dans le presse-papier) non encapsulée dans du HTML - case 'upload_image_base64': - return ImageUploadController::uploadImageBase64(); - - /* -- requêtes spécifiques au calendrier -- */ - case 'new_event': - return CalendarController::newEvent($json, $this->entityManager); - case 'update_event': - return CalendarController::updateEvent($json, $this->entityManager); - case 'remove_event': - return CalendarController::removeEvent($json, $this->entityManager); - - /* -- mode maintenance -- */ - case 'get_logs': - return MaintenanceController::getLogs($this->entityManager); - case 'erase_logs': - return MaintenanceController::eraseLogs($this->entityManager); - - default: - return new JsonResponse(['success' => false, 'error' => 'bad parameters'], JsonResponse::HTTP_BAD_REQUEST); - } - } - - /* -- site entier (header, footer, favicon) -- */ - elseif($this->request->query->has('head_foot_text')){ - return HeadFootController::setTextData($this->entityManager, $this->request->query->get('head_foot_text'), $json); - } - elseif($this->request->query->has('head_foot_social_check')){ - return HeadFootController::displaySocialNetwork($this->entityManager, $this->request->query->get('head_foot_social_check'), $json); - } - - /* -- page Menu et chemins -- */ - elseif($this->request->query->has('menu_edit')){ - // ne suit pas la règle, faire ça dans un contrôleur? - Model::$menu = new Menu($this->entityManager); // récupération des données - - // flèche gauche <=: position = position du parent + 1, parent = grand-parent, recalculer les positions - if($this->request->query->get('menu_edit') === 'move_one_level_up' && isset($json['id'])){ - return MenuAndPathsController::MoveOneLevelUp($this->entityManager, $json); - } - // flèche droite =>: position (léments de a fraterie + 1, l'élément précédent devient le parent - elseif($this->request->query->get('menu_edit') === 'move_one_level_down' && isset($json['id'])){ - return MenuAndPathsController::MoveOneLevelDown($this->entityManager, $json); - } - elseif($this->request->query->get('menu_edit') === 'switch_positions' && isset($json['id1']) && isset($json['id2'])){ - return MenuAndPathsController::switchPositions($this->entityManager, $json); - } - elseif($this->request->query->get('menu_edit') === 'display_in_menu' && isset($json['id']) && isset($json['checked'])){ - return MenuAndPathsController::displayInMenu($this->entityManager, $json); - } - elseif($this->request->query->get('menu_edit') === 'url_edit' && isset($json['id']) && isset($json['field']) && isset($json['input_data'])){ - return MenuAndPathsController::editUrl($this->entityManager, $json); - } - else{ - return new JsonResponse(['success' => false, 'error' => 'bad parameters'], JsonResponse::HTTP_BAD_REQUEST); // code 400 - } - } - - /* -- mode Modification d'une page -- */ - // partie "page" - elseif($this->request->query->has('page_edit')){ - switch($this->request->query->get('page_edit')){ - case 'page_title': - return PageManagementController::setPageTitle($this->entityManager, $json); - case 'page_description': - return PageManagementController::setPageDescription($this->entityManager, $json); - default: - return new JsonResponse(['success' => false, 'error' => 'bad parameters'], JsonResponse::HTTP_BAD_REQUEST); // code 400 - } - } - - // partie "blocs" - elseif($this->request->query->has('bloc_edit')){ - switch($this->request->query->get('bloc_edit')){ - case 'rename_page_bloc': - return PageManagementController::renameBloc($this->entityManager, $json); - case 'switch_blocs_positions': - return PageManagementController::SwitchBlocsPositions($this->entityManager, $json, $this->request->query->get('page')); - case 'change_articles_order': - return PageManagementController::changeArticlesOrder($this->entityManager, $json); - case 'change_presentation': - return PageManagementController::changePresentation($this->entityManager, $json); - case 'change_cols_min_width': - return PageManagementController::changeColsMinWidth($this->entityManager, $json); - case 'change_pagination_limit': - return PageManagementController::changePaginationLimit($this->entityManager, $json); - default: - return new JsonResponse(['success' => false, 'error' => 'bad parameters'], JsonResponse::HTTP_BAD_REQUEST); // code 400 - } - } - - else{ - return new JsonResponse(['success' => false, 'error' => 'bad parameters'], JsonResponse::HTTP_BAD_REQUEST); // code 400 - } - } - - /* -- upload avec FormData OU formulaire HTML avec fichier -- */ - elseif(str_starts_with($this->request->headers->get('Content-Type'), 'multipart/form-data')){ // = $_SERVER['CONTENT_TYPE'] - // dans tinymce avec le plugin (bouton "insérer une image" de l'éditeur ou glisser-déposer) - if($this->request->query->get('action') === 'upload_image_tinymce'){ - return ImageUploadController::imageUploadTinyMce(); - } - // dans tinymce, des quatre méthodes: bouton "link", drag & drop, html, base64 - elseif($this->request->query->get('action') === 'upload_file_tinymce'){ - return FileUploadController::fileUploadTinyMce(); - } - elseif($this->request->query->has('head_foot_image')){ - return HeadFootController::uploadAsset($this->entityManager, $this->request->query->get('head_foot_image')); - } - - /* -- page Maintenance -- */ - elseif($this->request->query->get('action') === 'restore_database' && $this->request->request->get('hidden') === '' - && $this->request->files->has('uploaded_sql')){ - return MaintenanceController::downloadSQL($entityManager, $request); - } - else{ - // choix ici entre répondre en JSON ou par une redirection, choix du JSON pour pouvoir passer un message - return new JsonResponse(['success' => false, 'error' => 'bad parameters'], JsonResponse::HTTP_BAD_REQUEST); // code 400 - } - } - - /* -- formulaire HTML sans fichier -- */ - elseif($this->request->headers->get('Content-Type') === 'application/x-www-form-urlencoded'){ - if($this->request->query->get('action') === 'delete_article' && $this->request->query->has('id')){ - return ArticleController::deleteArticle($this->entityManager, $this->request); // version formulaire - } - - /* -- nouvelle page -- */ - elseif($this->request->request->get('page_name') !== null - && $this->request->request->get('page_name_path') !== null - && $this->request->request->get('page_location') !== null - && $this->request->request->get('page_description') !== null - && $this->request->request->get('new_page_hidden') === ''){ - return PageManagementController::newPage($this->entityManager, $this->request->request); - } - - /* -- suppression d'une page -- */ - elseif($this->request->request->get('page_id') !== null - && $this->request->request->get('submit_hidden') === ''){ - return PageManagementController::deletePage($this->entityManager, $this->request->request->get('page_id')); - } - - - /* -- mode Modification d'une page -- */ - // modification du chemins en snake_case - elseif($this->request->request->get('page_menu_path') !== null - && $this->request->request->get('page_id') !== null - && $this->request->request->get('page_name_path_hidden') === ''){ - return PageManagementController::updatePageMenuPath($this->entityManager, $this->request->request->get('page_menu_path')); - } - // ajout d'un bloc dans une page - elseif($this->request->request->get('bloc_title') !== null - && $this->request->request->get('bloc_select') !== null - && $this->request->request->get('bloc_title_hidden') === ''){ // contrôle anti-robot avec input hidden - return PageManagementController::addBloc($this->entityManager, $this->request); - } - // suppression d'un bloc de page - elseif($this->request->request->get('delete_bloc_id') !== null - && $this->request->request->get('delete_bloc_hidden') === ''){ // contrôle anti-robot avec input hidden - return PageManagementController::deleteBloc($this->entityManager, $this->request); - } - - - /* -- page Menu et chemins -- */ - // création d'une entrée de menu avec une URL - elseif($this->request->request->has("label_input") && $this->request->request->has("url_input") && $this->request->request->has("location")){ - return MenuAndPathsController::newUrlMenuEntry($this->entityManager); - } - // suppression d'une entrée de menu avec une URL - elseif($this->request->request->has('delete') && $this->request->request->has('x') && $this->request->request->has('y')){ // 2 params x et y sont là parce qu'on a cliqué sur une image - return MenuAndPathsController::deleteUrlMenuEntry($this->entityManager); - } - - - /* -- page Mon compte -- */ - elseif($this->request->query->get('action') === 'update_username'){ - return UserController::updateUsername($this->entityManager); - } - elseif($this->request->query->get('action') === 'update_password'){ - return UserController::updatePassword($this->entityManager); - } - - /* -- page Maintenance -- */ - elseif($this->request->query->get('action') === 'restore_database' && $this->request->get('hidden') === '' && $this->request->request->has('selected_sql')){ - return MaintenanceController::handleBackupSelection($this->entityManager, $this->request); - } - - // redirection page d'accueil - return new RedirectResponse((string)new URL(['error' => 'paramètres inconnus'])); - } - - // requêtes XMLHttpRequest - elseif($this->request->isXmlHttpRequest()){ - return new JsonResponse(['success' => false]); // noyer le poisson en laissant penser que le site gère les requêtes XHR - } - - // POST admin ne matchant pas - return new Response('bad parameters', Response::HTTP_BAD_REQUEST); // code 400 - } - - // POST non-admin ne matchant pas - return new Response('bad parameters', Response::HTTP_BAD_REQUEST); // code 400 - } - - // méthode HTTP inconnue - else{ - return new RedirectResponse((string)new URL(['error' => 'tu fais quoi là mec?'])); - } - } -} \ No newline at end of file diff --git a/src/service/Security.php b/src/service/Security.php deleted file mode 100644 index 356f4f4..0000000 --- a/src/service/Security.php +++ /dev/null @@ -1,110 +0,0 @@ -1, // protection contre les élements et attributs dangereux - - // liste blanche d'éléments HTML - 'elements'=> 'h1, h2, h3, h4, h5, h6, p, br, s, em, span, strong, a, ul, ol, li, sup, sub, code, blockquote, div, pre, table, caption, colgroup, col, tbody, tr, th, td, figure, img, figcaption, iframe, small', - - // liste noire d'attributs HTML - 'deny_attribute'=> 'id, class' // on garde 'style' - ); - // faire qu'un certain élément puisse n'avoir que certains attributs, regarder la doc - private static $specHtmLawed = ''; - - // obtenir du HTML non dangereur sans appliquer htmlspecialchars - public static function secureHTML(string $chaine): string - { - return trim(htmLawed($chaine, self::$configHtmLawed, self::$specHtmLawed)); - } - - public static function secureFileName(string $chaine): string - { - // sécuriser un nom avec chemin avec basename? - //$chaine = basename($chaine); - - /* - - caractères interdits sous windows / \ : * ? " < > | - - mac autorise les / - - mac interdit : - - linux autorise tout sauf les / - - imagemagick ne supporte pas les : - - - 'espace' fonctionne - - / remplacé par firefox en : - - \ retire ce qui est devant le \ - - * fonctionne - - ? permet le téléchargement mais pas l'affichage - - " ne fonctionne pas, remplacé par %22, filtrer %22 - - < > fonctionnent - - | fonctionne - - = fonctionne, mais je filtre parce qu'on en trouve dans une URL - - ' ` fonctionnent - - % fonctionne - - (){}[] fonctionnent - - ^ fonctionne - - # ne fonctionne pas - - ~ fonctionne - - & fonctionne - - ^ pas encore testé - */ - - // => on remplace tout par des _ - // filtrer / et \ semble inutile - - /*$cibles = [' ', '/', '\\', ':', '*', '?', '<', '>', '|', '=', "'", '`', '"', '%22', '#']; - $chaine = str_replace($cibles, '_', $chaine); // nécéssite l'extension mbstring - $chaine = mb_strtolower($chaine); - return($chaine);*/ - - $chaine = preg_replace('/[^a-zA-Z0-9_-]/', '_', $chaine); // ne garder que les lettres, chiffres, tirets et underscores - $chaine = preg_replace('/_+/', '_', $chaine); // doublons d'underscores - return trim($chaine, '_'); - - // les problèmes avec \ persistent !! - // => javascript - // malheureusement document.getElementById('upload').files[0].name = chaine; ne marche pas! interdit! - // javascript ne doit pas pouvoir accéder au système de fichiers - // solutions: - // - au lieu de fournir une chaine (le chemin du fichier), donner un objet à files[0].name - // - créer une copie du fichier et l'envoyer à la place - // - envoyer le fichier en AJAX - // - envoyer le nom du fichier à part puis renommer en PHP - } -} - -// lien sans http:// -function fixLinks($data) -{ - // 1/ - // si une adresse est de type "domaine.fr" sans le http:// devant, le comportement des navigateurs est de rechercher un fichier comme si mon adresse commençait par file:// - // tomber ainsi sur une page d'erreur est parfaitement déroutant - - // regex pour détecter les balises
    et ajouter http:// au début des liens si nécessaire - $pattern = '#(]+href=")((?!https?://)[^>]+>)#'; - //$data = preg_replace($pattern, '$1http://$2', $data); - - // 2/ - // cas où la regex fait mal son boulot: - // l'erreur 404 est gérée par le .htaccess - // et le visiteur est redirigé à la page "menu" - // (ça ne règle pas le problème mais c'est mieux) - - // 3/ - // quand l'éditeur est ouvert (avant de valider l'article), - // le lien qu'on vient de créer apparaît dans l'infobulle, - // cliquer dessus ouvre un onglet sur une erreur 404 - // solution partielle avec le .htaccess - // - // solution? fermer ce nouvel onglet avec echo ''; - // comment déclencher le JS? en faisant qu'une erreur 404 causée pour cette raison soit particulière? - - return($data); -} diff --git a/src/service/URL.php b/src/service/URL.php deleted file mode 100644 index 5bd2594..0000000 --- a/src/service/URL.php +++ /dev/null @@ -1,88 +0,0 @@ -params = $gets; - if($anchor != ''){ - $this->setAnchor($anchor); - } - } - - // setters statiques - static public function setProtocol(string $protocol = 'http'): void - { - self::$protocol = $protocol === 'https' ? 'https://' : 'http://'; - } - static public function setPort(int|string $port = 80): void - { - if((int)$port === 443){ - self::$protocol = 'https://'; - self::$port = ''; - } - elseif((int)$port === 80){ - self::$protocol = 'http://'; - self::$port = ''; - } - else{ - self::$port = ':' . (string)$port; - } - } - static public function setHost(string $host): void - { - self::$host = $host; - } - static public function setPath(string $path): void - { - self::$path = '/' . ltrim($path, '/'); - } - - //setters normaux - public function addParams(array $gets): void - { - // array_merge est préféré à l'opérateur d'union +, si une clé existe déjà la valeur est écrasée - $this->params = array_merge($this->params, $gets); - } - public function setAnchor(string $anchor = ''): void - { - if($anchor != ''){ - $this->anchor = '#' . ltrim($anchor, '#'); - } - else{ - $this->anchor = ''; - } - } - - private function makeParams(): string - { - $output = ''; - $first = true; - - foreach($this->params as $key => $value) { - if($first){ - $output .= '?'; - $first = false; - } - else{ - $output .= '&'; - } - $output .= $key . '=' . $value; - } - return $output; - } - - public function __toString(): string - { - return self::$protocol . self::$host . self::$port . self::$path . $this->makeParams() . $this->anchor; - } -} \ No newline at end of file diff --git a/src/service/User.php b/src/service/User.php deleted file mode 100644 index bd17dce..0000000 --- a/src/service/User.php +++ /dev/null @@ -1,23 +0,0 @@ -createQuery("SELECT u FROM App\Entity\User u") - ->setMaxResults(1) - ->getOneOrNullResult()) - { - unset($_SESSION['user']); - return false; - } - else{ - return true; - } - } -} \ No newline at end of file diff --git a/src/service/UserDataService.php b/src/service/UserDataService.php deleted file mode 100644 index e0156e2..0000000 --- a/src/service/UserDataService.php +++ /dev/null @@ -1,46 +0,0 @@ -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/session.php b/src/service/session.php deleted file mode 100644 index 24023e9..0000000 --- a/src/service/session.php +++ /dev/null @@ -1,90 +0,0 @@ - 'session_invalide'])); - die; - } - - // MAJ de la session avec CERTAINES données - $_SESSION['user']['username'] = $user->getLogin(); - $_SESSION['user']['role'] = $user->getRole(); - - $is_admin = $user->getRole() === 'admin'; - } - - define('IS_ADMIN', $is_admin); - - // si on a un jour besoin d'une variable globale au lieu d'une constante - //$GLOBALS['is_admin'] = $is_admin; // version modifiable 1 - /*function isAdmin(): bool { // version modifiable 2 - return $_SESSION['user']['role'] ?? null === 'admin'; - }*/ - - - // => système de cache à ajouter pour ne pas lire la BDD à chaque fois - //remplacer ce qui est en haut - /*$user = $_SESSION['user'] ?? null; - if (!$user) { - // visiteur - } - // Vérification périodique (ex: toutes les 5 minutes) - if (time() - $user['last_check'] > 300) { - $user = UserController::getUserById($user['id'], $entityManager); - if (!$user) { - session_destroy(); - header('Location: /login.php'); - exit; - } - // cache pour ne pas avoir à lire la BDD à chaque page - $_SESSION['user'] = [ - 'id' => $user['id'], - 'role' => $user['role'], - 'username' => $user['username'], - 'last_check' => time() - ]; - $user = $_SESSION['user']; - } - $is_admin = ($user['role'] === 'admin');*/ - - - // améliorations possibles: ajouter expiration automatique + protection contre vol de session (IP / user-agent) sans casser ton app. -} - -// nettoyage complet -/*function cleanSession(){ - unset($_SESSION['user']); // mémoire vive - session_destroy(); // fichier côté serveur - setcookie('PHPSESSID', '', time() - 86400, '/'); // cookie de session -}*/ \ No newline at end of file diff --git a/src/view/AbstractBuilder.php b/src/view/AbstractBuilder.php deleted file mode 100644 index b9a1d7f..0000000 --- a/src/view/AbstractBuilder.php +++ /dev/null @@ -1,77 +0,0 @@ -id_node = $node->getId(); - } - - protected function useChildrenBuilder(Node $node): void - { - foreach($node->getChildren() as $child_node) - { - $builder_name = $this->snakeToPascalCase($child_node->getName()) . 'Builder'; - $builder = new $builder_name($child_node); - $this->html .= $builder->render(); - - // pages spéciales où on n'assemble pas tout - if($builder_name === 'HeadBuilder' && in_array(Model::$page_path->getString(), ['connection', 'user_edit'])) - { - foreach($node->getChildren() as $target_node){ - if($target_node->getName() === 'main'){ - $main_node = $target_node; - break; - } - } - // on construit
    et on s'arrête! les autres noeuds sont ignorés - $builder_name = $this->snakeToPascalCase($main_node->getName()) . 'Builder'; - $builder = new $builder_name($main_node); - $this->html .= "\n"; - $this->html .= $builder->render() . "\n"; - $this->html .= "\n"; - break; - } - } - } - - private function snakeToPascalCase(string $input): string - { - return str_replace('_', '', ucwords($input, '_')); - } - - public function render(): string // = getHTML(), nécéssite d'être public - { - return $this->html; - } - protected function addHTML(string $html): void - { - $this->html .= $html; - } - - protected function insertSVG(string $path, array $attributes = []): string - { - $svg = file_get_contents($path); - - // modification des attributs - if(!empty($attributes)){ - $dom = new DOMDocument(); - $dom->loadXML($svg); - $svg_elem = $dom->documentElement; - foreach($attributes as $key => $value){ - $svg_elem->setAttribute($key, $value); - } - $svg = $dom->saveXML($svg_elem); - } - return $svg; - } -} \ No newline at end of file diff --git a/src/view/BreadcrumbBuilder.php b/src/view/BreadcrumbBuilder.php deleted file mode 100644 index 2655551..0000000 --- a/src/view/BreadcrumbBuilder.php +++ /dev/null @@ -1,50 +0,0 @@ -html = $this->breadcrumbHTML(); - } - - private function breadcrumbHTML(): string - { - $asset = 'assets/home.svg'; // => BDD? - $breadcrumb_array = Model::$page_path->getArray(); // tableau de Page - $html = ''; - $nb_of_entries = count($breadcrumb_array); - - if($nb_of_entries > 1) - { - // petite maison et flèche - $html .= '\n"; - } - return $html; - } -} \ No newline at end of file diff --git a/src/view/CalendarBuilder.php b/src/view/CalendarBuilder.php deleted file mode 100644 index 0c0006d..0000000 --- a/src/view/CalendarBuilder.php +++ /dev/null @@ -1,29 +0,0 @@ -getName() . '.php'; - - $calendar_js_file = IS_ADMIN ? 'calendar_admin' : 'calendar'; - - if(file_exists($viewFile)) - { - if(!empty($node->getNodeData()->getData())) - { - extract($node->getNodeData()->getData()); - } - - ob_start(); - require $viewFile; - $this->html = ob_get_clean(); // pas de concaténation ici, on écrase - } - } -} \ No newline at end of file diff --git a/src/view/FooterBuilder.php b/src/view/FooterBuilder.php deleted file mode 100644 index bb99c13..0000000 --- a/src/view/FooterBuilder.php +++ /dev/null @@ -1,117 +0,0 @@ -getName() . '.php'; - - if(file_exists($viewFile)) - { - $node_data = $node->getNodeData(); - // nom du contact, adresse et e-mail - if(!empty($node_data->getData())) - { - extract($node_data->getData()); - } - - $footer_logo = Asset::USER_PATH . $node_data->getAssetByRole('footer_logo')?->getFileName() ?? ''; - - $this->useChildrenBuilder($node); - $breadcrumb = $this->html; - - $empty_admin_zone = ''; - if(IS_ADMIN) - { - // données du footer - $admin_footer_name = ' - - - '; - $admin_footer_address = ' - - - '; - $admin_footer_email = ' - - - '; - - $admin_footer_logo = ' - - - '; - - // zone admin - $empty_admin_zone = 'empty_admin_zone'; - if(MainBuilder::$modif_mode){ - $mode = 'modification de page'; - $div_admin = 'logged_in modif_mode'; - } - else{ - $mode = 'administrateur'; - $div_admin = 'logged_in'; - } - - $link_change_password = new URL(['page' => 'user_edit', 'from' => CURRENT_PAGE]); - isset($_GET['id']) ? $link_change_password->addParams(['id' => $_GET['id']]) : ''; - - $link_logout = new URL(['action' => 'deconnection', 'from' => CURRENT_PAGE]); - isset($_GET['id']) ? $link_logout->addParams(['id' => $_GET['id']]) : ''; - - $zone_admin = '
    -

    Vous êtes en mode ' . $mode . ".

    \n" . - '' . "\n" . - $this->makePageModifModeButton() . "\n" . - '' . "\n" . - '' . "\n" . - '' . "\n" . - '' . "\n" . - '
    ' . "\n"; - } - else - { - $div_admin = 'logged_out'; - $url = new URL(['page' => 'connection', 'from' => CURRENT_PAGE]); - if(Model::$page_path->getLast()->getEndOfPath() === 'article' && isset($_GET['id'])){ - $url->addParams(['id' => $_GET['id']]); - } - $zone_admin = ''; - - $admin_footer_name = ''; - $admin_footer_address = ''; - $admin_footer_email = ''; - $admin_footer_logo = ''; - } - - ob_start(); - require $viewFile; - $this->html = ob_get_clean(); - } - } - - private function makePageModifModeButton(): string - { - $link_edit_page = new URL(['page' => CURRENT_PAGE]); - if(!in_array(CURRENT_PAGE, ViewController::SPECIAL_PAGES)) // cas impossibles: 'user_edit' et 'connection' qui n'ont pas de footer - { - if(MainBuilder::$modif_mode){ - $link_edit_label = 'Sortir du mode modification'; - } - else{ - $link_edit_page->addParams(['mode' => 'page_modif']); - $link_edit_label = 'Modifier la page'; - } - return ''; - } - else{ - return ''; - } - } -} \ No newline at end of file diff --git a/src/view/FormBuilder.php b/src/view/FormBuilder.php deleted file mode 100644 index 9a900ce..0000000 --- a/src/view/FormBuilder.php +++ /dev/null @@ -1,50 +0,0 @@ -getNodeData()->getData())){ - extract($node->getNodeData()->getData()); - } - - // un seul captcha à la fois! - if(!self::$captcha){ - self::$captcha = new Captcha; - $_SESSION['captcha'] = self::$captcha->getSolution(); - } - - // données stockées en vrac dans du JSON et récupérées avec extract => changer ça un jour - $smtp_host = $smtp_host ?? Config::$smtp_host; - $smtp_secure = $smtp_secure ?? Config::$smtp_secure; - $smtp_username = $smtp_username ?? Config::$smtp_username; - $email_dest = $email_dest ?? Config::$email_dest; - $keep_emails = (bool)($keep_emails ?? false); // (bool) est inutile mais plus clair - $retention_period = $this->getRetentionPeriod($retention_period ?? null, App\Entity\Email::DEFAULT_RETENTION_PERIOD); - $retention_period_sensible = $this->getRetentionPeriod($retention_period_sensible ?? null, App\Entity\Email::DEFAULT_RETENTION_PERIOD_SENSITIVE); - - $admin_content = ''; - if(IS_ADMIN){ - ob_start(); - require self::VIEWS_PATH . 'form_admin.php'; - $admin_content = ob_get_clean(); - } - - ob_start(); - require self::VIEWS_PATH . $node->getName() . '.php'; - $this->html = ob_get_clean(); // pas de concaténation ici, on écrase - } - - private function getRetentionPeriod(mixed $period, int $default_period): int - { - return ($period === null || (int)$period <= 0) ? $default_period : (int)$period; // (int) est nécessaire à cause du stockage JSON - } -} \ No newline at end of file diff --git a/src/view/GaleryBuilder.php b/src/view/GaleryBuilder.php deleted file mode 100644 index 019a2c7..0000000 --- a/src/view/GaleryBuilder.php +++ /dev/null @@ -1,70 +0,0 @@ -getName() . '.php'; - - if(file_exists($viewFile)) - { - if(!empty($node->getNodeData()->getData())) - { - extract($node->getNodeData()->getData()); - } - - // ajouter un article - $new_article = ''; - if(IS_ADMIN) - { - $id = 'n' . $this->id_node; - $js = 'onclick="openEditor(\'' . $id . '\')"'; - - $share_button = ''; - $html = ''; - - $new_button = '

    ' . "\n" . - '

    '; - - $modify_js = 'onclick="openEditor(\'' . $id . '\')"'; - $modify_article = '' . "\n"; - - $up_js = 'onclick="switchPositions(\'' . $id . '\', \'up\')"'; - $up_button = '' . "\n"; - - $down_js = 'onclick="switchPositions(\'' . $id . '\', \'down\')"'; - $down_button = '' . "\n"; - - $delete_js = 'onclick="deleteArticle(\'' . $id . '\')"'; - $delete_article = '' . "\n"; - - $close_js = 'onclick="closeEditor(\'' . $id . '\')"'; - $close_editor = ''; - - $submit_js = 'onclick="submitArticle(\'' . $id . '\', \'\', clone' . $this->id_node . ')"'; - $submit_article = ''; - - $admin_buttons = $new_button . $modify_article . $up_button . $down_button . $delete_article . $close_editor . $submit_article; - - // squelette d'un nouvel article - ob_start(); - require self::VIEWS_PATH . 'post.php'; - $new_article = ob_get_clean(); - } - - // articles existants - $this->useChildrenBuilder($node); - $content = $this->html; - - ob_start(); - require $viewFile; - $this->html = ob_get_clean(); // pas de concaténation ici, on écrase - } - } -} \ No newline at end of file diff --git a/src/view/HeadBuilder.php b/src/view/HeadBuilder.php deleted file mode 100644 index 9cb0650..0000000 --- a/src/view/HeadBuilder.php +++ /dev/null @@ -1,81 +0,0 @@ -getName() . '.php'; - - if(file_exists($viewFile)){ - // css et js - $page = Model::$page_path->getLast(); - - $css = ''; - foreach($page->getCSS() as $name){ - $css .= self::insertCSS($name); - } - - $js = ''; - foreach($page->getJS() as $name){ - $js .= self::insertJS($name); - } - - if(MainBuilder::$modif_mode){ - $css .= self::insertCSS('modif_page'); - $js .= self::insertJS('modif_page'); - } - - if(IS_ADMIN){ - // édition éléments sur toutes les pages (header, footer et favicon) - $js .= self::insertJS('Input'); - - // sert partout? - $js .= self::insertJS('Fetcher'); - - // tinymce, nécéssite un script post-install et post-update dans composer.json - $css .= self::insertCSS('tinymce'); - $js .= self::insertJS('tinymce/tinymce.min'); - $js .= self::insertJS('tinymce'); - } - - $title = Model::$page_path->getLast()->getPageName(); - $description = Model::$page_path->getLast()->getDescription(); - - // favicon - // ?-> est l'opérateur de navigation sécurisée => LOVE! - $favicon_name = ($favicon_object = $node->getNodeData()->getAssetByRole('head_favicon'))?->getFileName(); - $favicon = $favicon_name ? Asset::USER_PATH . $favicon_name : ''; - $favicon_type = $favicon_object?->getMimeType() ?? ''; - - ob_start(); - require $viewFile; - $this->html .= ob_get_clean(); - } - } - - static function insertCSS(string $name): string - { - return '' . "\n"; - } - static function insertJS(string $name, bool $module = false): string - { - return '' . "\n"; - } - - static function versionedFileURL(string $type, string $filename): string - { - $path = $type . '/' . $filename . '.' . $type; - - if(file_exists($path)){ - $version = substr(md5_file($path), 0, 8); - return $path . '?v=' . $version; - } - return $path; // sécurité fichier absent - } -} diff --git a/src/view/HeaderBuilder.php b/src/view/HeaderBuilder.php deleted file mode 100644 index bfd5963..0000000 --- a/src/view/HeaderBuilder.php +++ /dev/null @@ -1,129 +0,0 @@ -getChildren(); - foreach($children as $child) - { - if($child->getName() === 'nav'){ - $this->nav = $child; - // actuellement le noeud nav ne contient aucune info utile et l'envoyer à NavBuilder est inutile - $nav_builder = new NavBuilder($this->nav); - - $nav = $nav_builder->render(); - } - elseif($child->getName() === 'breadcrumb'){ - $this->breadcrumb = $child; - $breadcrumb_builder = new BreadcrumbBuilder($this->breadcrumb); - $breadcrumb = $breadcrumb_builder->render(); - } - } - - $viewFile = self::VIEWS_PATH . $node->getName() . '.php'; - - if(file_exists($viewFile)) - { - $node_data = $node->getNodeData(); - // titre et description - if(!empty($node_data->getData())) - { - extract($node_data->getData()); - } - - // réseaux sociaux + logo dans l'entête - // ?-> est l'opérateur de chainage optionnel - $header_logo = Asset::USER_PATH . $node_data->getAssetByRole('header_logo')?->getFileName() ?? ''; - $header_background_name = $node_data->getAssetByRole('header_background')?->getFileName(); - $header_background = $header_background_name ? Asset::USER_PATH . $header_background_name : ''; - $social_networks = ''; - - // boutons mode admin - if(IS_ADMIN){ - // assets dans classe header_additional_inputs - $admin_head_favicon = ' - - - '; - $admin_header_background = ' - - - '; - - // asset dans classe header_content - $admin_header_logo = ' - - - '; - - // texte dans classe header_content - $admin_header_title = ' - - - '; - $admin_header_description = ' - - - '; - - // icônes réseaux sociaux - // boucle sur la liste complète de réseaux sociaux - foreach(NodeData::$social_networks as $network){ - $checked = (isset($social_show[$network]) && $social_show[$network]) ? 'checked' : ''; - $href = (isset($social[$network]) && $social[$network] !== '') ? 'href="' . $social[$network] . '"' : ''; - - $social_networks .= '
    - - ' - . $this->insertSVG(self::ICON_PATH . $network . '.svg', ['id' => 'header_' . $network . '_content', 'class' => ($checked ? 'svg_fill_red' : '')]) - . ' - - - - - -
    '; - // {'has_content': false} => InputToggle ne gèrera pas cette balise - } - } - else{ - $admin_head_favicon = ''; - $admin_header_background = ''; - $admin_header_logo = ''; - $admin_header_title = ''; - $admin_header_description = ''; - - if(isset($social_show)){ - // boucle sur les réseaux sociaux "activés" - foreach(array_keys($social_show) as $network){ - if($social_show[$network]){ - $href = (isset($social[$network]) && $social[$network] !== '') ? 'href="' . $social[$network] . '"' : ''; - $social_networks .= ''; - } - } - } - } - - ob_start(); - require $viewFile; - $this->html .= ob_get_clean(); - } - } -} \ No newline at end of file diff --git a/src/view/LoginBuilder.php b/src/view/LoginBuilder.php deleted file mode 100644 index 479398c..0000000 --- a/src/view/LoginBuilder.php +++ /dev/null @@ -1,52 +0,0 @@ -getName() . '.php'; - - $captcha = new Captcha; - $_SESSION['captcha'] = $captcha->getSolution(); // enregistrement de la réponse du captcha pour vérification - - //$this->html .= $header; - - $error_messages = [ - 'error_non_valid_captcha' => 'Erreur au test anti-robot, veuillez saisir un nombre entier.', - 'captcha_server_error' => 'captcha_server_error', - 'bad_solution_captcha' => 'Erreur au test anti-robot, veuillez réessayer.', - 'bad_login_or_password' => 'Mauvais identifiant ou mot de passe, veuillez réessayer.', // ne pas indiquer où est l'erreur - 'forbidden_characters' => 'Caractères interdits: espaces, tabulations, sauts CR/LF.' - ]; - - $link_form = new URL(['action' => 'connection']); - isset($_GET['from']) ? $link_form->addParams(['from' => $_GET['from']]) : ''; - isset($_GET['id']) ? $link_form->addParams(['id' => $_GET['id']]) : ''; - - $link_exit = new URL; - isset($_GET['from']) ? $link_exit->addParams(['page' => $_GET['from'] ]) : ''; - isset($_GET['id']) ? $link_exit->addParams(['id' => $_GET['id']]) : ''; - - $error = isset($_GET['error']) ? $error_messages[$_GET['error']] : ''; - - ob_start(); - require $viewFile; - $this->html = ob_get_clean(); // nouveau contenu - - //$this->html .=

    Ce site utilise un cookie « obligatoire » lorsque vous êtes connecté ainsi que sur cette page.
    Il sera supprimé à votre déconnexion ou dès que vous aurez quitté le site.

    ; - - //$this->html .= $footer; - } -} diff --git a/src/view/MainBuilder.php b/src/view/MainBuilder.php deleted file mode 100644 index 1df5846..0000000 --- a/src/view/MainBuilder.php +++ /dev/null @@ -1,95 +0,0 @@ -html .= "
    \n"; - - // page article: cas particulier où l'article est greffé sur main - if(Model::$page_path->getLast()->getEndOfPath() === 'article'){ - // pas censé arriver - if(!isset($_GET['id'])){ - header('Location: ' . new URL); - die; - } - - // nouvel article - if($node->getAdoptedChild() == null){ - $date = new \DateTime; - $article = new Article('', $date); - $new = new Node('new', 0, null, null, $article); - } - // modification - else{ - $new = $node->getAdoptedChild(); - } - //$builder_name = $this->snakeToPascalCase($new->getName()) . 'Builder'; - $builder_name = 'NewBuilder'; - $builder = new $builder_name($new); - $this->html .= $builder->render(); - } - else{ - // si action = "modif_page", affiche des commandes supplémentaires - if(IS_ADMIN && self::$modif_mode){ - // ajouter un contrôle du champ in_menu - $this->viewEditBlocks($node); - } - - // dans tous les cas - $this->useChildrenBuilder($node); - } - - $this->html .= "
    \n"; - } - - // mode modification de page uniquement - private function viewEditBlocks(Node $node): void - { - $options = ''; - foreach(Presentation::BLOCKS as $key => $value){ - $options .= '\n"; - } - - // ceci pourrait être déplacé au début des blocs - $bloc_edit = ''; - foreach($node->getChildren() as $child_node){ - // ordre des articles 'news' - if($child_node->getName() === 'news_block'){ - $order = $child_node->getNodeData()->getChronoOrder() ? 'chrono' : 'antichrono'; - } - - // présentation par défaut - if(Presentation::hasPresentation($child_node->getName()) && $child_node->getNodeData()->getPresentation() === null){ - $child_node->getNodeData()->setPresentation('full_width'); // pas de persistence ici - } - - ob_start(); - require self::VIEWS_PATH . 'modify_block.php'; - $bloc_edit .= ob_get_clean(); - } - - ob_start(); - require self::VIEWS_PATH . 'modify_page.php'; - $this->html .= ob_get_clean(); - } - - // utilisée dans modify_block.php - private function makePresentationOptions(string $presentation): string - { - $options = ''; - foreach(Presentation::PRESENTATIONS as $key => $value){ - $options .= ''; - } - return $options; - } -} \ No newline at end of file diff --git a/src/view/MaintenanceBuilder.php b/src/view/MaintenanceBuilder.php deleted file mode 100644 index 5ed1ce2..0000000 --- a/src/view/MaintenanceBuilder.php +++ /dev/null @@ -1,33 +0,0 @@ -getName() . '.php'; - - // noter qu'un backup a été créé dans UserController::connect() - try{ - $backup_array = Backup::getBackupList(); - } - catch(RuntimeException $e){ - $backup_array = []; - $_SESSION['flash_message'] = $e->getMessage(); // peut écraser une flash error déjà dans $_SESSION['flash_message'] - } - - $backup_options = ''; - for($i = count($backup_array) - 1; $i >= 0; $i--){ - $backup_options .= ''; - } - - if(file_exists($viewFile)){ - ob_start(); - require $viewFile; - $this->html = ob_get_clean(); - } - } -} \ No newline at end of file diff --git a/src/view/MenuBuilder.php b/src/view/MenuBuilder.php deleted file mode 100644 index 0395dfa..0000000 --- a/src/view/MenuBuilder.php +++ /dev/null @@ -1,97 +0,0 @@ -getName() . '.php'; - - if(file_exists($viewFile)) - { - if(IS_ADMIN){ - $this->unfoldMenu(Model::$menu); - - if($template){ - $this->unfoldOptions(Model::$menu); - } - } - else{ - header('Location: ' . new URL); - die; - } - - // si faux, n'utilise pas le template - if($template){ - ob_start(); - require $viewFile; // insertion de $this->html généré par unfoldMenu - $this->html = ob_get_clean(); // pas de concaténation .= cette fois on écrase - } - } - } - - private function unfoldMenu(Page $page): void - { - $this->html .= '
    ' . "\n"; - - foreach($page->getChildren() as $entry) - { - $checked = $entry->isHidden() ? '' : 'checked'; - $this->html .= '
    - - - - - - - '; - - // entrées URL: bouton poubelle + nom du lien + adresse - if(str_starts_with($entry->getEndOfPath(), 'http')){ - $this->html .= '
    - - -
    - - - - - - - - '; - } - else{ - $this->html .= ' - ' . $entry->getPagePath() . ''; - } - - if(count($entry->getChildren()) > 0){ - $this->unfoldMenu($entry); - } - $this->html .= '
    ' . "\n"; - } - $this->html .= "
    \n"; - } - - private function unfoldOptions(Page $page): void - { - foreach($page->getChildren() as $entry){ - $this->options .= '\n"; - if(count($entry->getChildren()) > 0){ - $this->unfoldOptions($entry); - } - } - } -} \ No newline at end of file diff --git a/src/view/NavBuilder.php b/src/view/NavBuilder.php deleted file mode 100644 index 6caefbc..0000000 --- a/src/view/NavBuilder.php +++ /dev/null @@ -1,73 +0,0 @@ -html .= ''; - } - - private function navMainHTML(Page $nav_data, array $current): string - { - $nav_html = ''; - static $level = 0; - - foreach($nav_data->getChildren() as $data) - { - if(!$data->isHidden()){ - $li_class = ''; - if(isset($current[$level]) && $data->getEndOfPath() === $current[$level]->getEndOfPath()){ - $li_class = 'current '; - } - - $link = ''; - if($data->isReachable()) // titre de catégorie du menu non clicable - { - if(str_starts_with($data->getEndOfPath(), 'http')) // lien vers autre site - { - $link .= ''; - } - elseif($data->getEndOfPath() != '') // lien relatif - { - $link .= ''; - } - } - else{ - $link .= ''; - } - - if(count($data->getChildren()) > 0) // titre de catégorie - { - $li_class .= $data->getParent() == null ? 'drop-down' : 'drop-right'; - - $nav_html .= '
  • ' . $link . '

    ' . $data->getPageName() . '

    - -
  • ' . "\n"; - } - else - { - $nav_html .= '
  • ' . $link . '

    ' . $data->getPageName() . '

  • ' . "\n"; - } - } - } - return $nav_html; - } -} \ No newline at end of file diff --git a/src/view/NewBuilder.php b/src/view/NewBuilder.php deleted file mode 100644 index 7459cc8..0000000 --- a/src/view/NewBuilder.php +++ /dev/null @@ -1,152 +0,0 @@ -getName() . '.php'; - - if(file_exists($viewFile)) - { - if(self::$new_article_mode){ - $id = $_GET['id']; // ici l'id est le nom du block news_block parent - $title = ''; - $preview = ''; - - // lettre au début de l'id: t = title, p = preview, i = article, d = date - $id_title = $id; - $id_title[0] = 't'; - $id_preview = $id; - $id_preview[0] = 'p'; - $id_content = 'i' . $id; - $id_content[0] = 'i'; - $id_date = $id; - $id_date[0] = 'd'; - } - else{ - $id = (string)$node->getId(); - - // html, date - $title = $node->getArticle()->getTitle(); - $preview = $node->getArticle()->getPreview(); - - // lettre au début de l'id: t = title, p = preview, i = article, d = date - $id_title = 't' . $id; - $id_preview = 'p' . $id; - $id_content = 'i' . $id; - $id_date = 'd' . $id; - } - - $content = ''; - if(CURRENT_PAGE === 'article'){ - $content = $node->getArticle()->getContent(); - $from_to_button = '

    '; - $overflow = ''; - } - else{ - $from_to_button = '

    '; - $overflow = ' overflow_hidden'; - } - - $date = $node->getArticle()->getDateTime()->format('Y-m-d\TH:i:s.v\Z'); // format: 2025-07-17T13:54:00.000Z - // format(\DateTime::ATOM) produit le format: 2025-10-10T12:17:00+00:00, c'est aussi de la norme ISO, mais à éviter pour être compatible avec date.toISOString en JS - - // partage - $share_link = new URL(['page' => 'article', 'id' => $id]); - $share_js = 'onclick="copyInClipBoard(\'' . $share_link . '\')"'; - if(isset($_GET['id']) && $_GET['id'][0] === 'n'){ - $class = 'class="share hidden"'; - } - else{ - $class = 'class="share"'; - } - $share_button = '

    ' . "\n"; - - // modifier un article - $title_buttons = ''; - $preview_buttons = ''; - $article_buttons = ''; - $date_buttons = ''; - $admin_buttons = ''; - if(IS_ADMIN){ - if(CURRENT_PAGE === 'article'){ - $title_js = 'onclick="openEditor(\'' . $id_title . '\')"'; - $modify_title = '

    ' . "\n"; - $close_js_title = 'onclick="closeEditor(\'' . $id_title . '\')"'; - $close_editor_title = ''; - $submit_js_title = 'onclick="submitArticle(\'' . $id_title . '\')"'; - $submit_title = ''; - $title_buttons = '
    ' . $modify_title . $close_editor_title . $submit_title . '
    '; - - $preview_js = 'onclick="openEditor(\'' . $id_preview . '\')"'; - $modify_preview = '

    ' . "\n"; - $close_js_preview = 'onclick="closeEditor(\'' . $id_preview . '\')"'; - $close_editor_preview = ''; - $submit_js_preview = 'onclick="submitArticle(\'' . $id_preview . '\')"'; - $submit_preview = ''; - $preview_buttons = '
    ' . $modify_preview . $close_editor_preview . $submit_preview . '
    '; - - $article_js = 'onclick="openEditor(\'' . $id_content . '\')"'; - $modify_article = '

    ' . "\n"; - $close_js_article = 'onclick="closeEditor(\'' . $id_content . '\')"'; - $close_editor_article = ''; - $submit_js_article = 'onclick="submitArticle(\'' . $id_content . '\')"'; - $submit_article = ''; - $article_buttons = '
    ' . $modify_article . $close_editor_article . $submit_article . '
    '; - - $date_js = 'onclick="openDatetimeLocalInput(\'' . $id_date . '\', \'article\');'; - $modify_date = '

    ' . "\n"; - $close_js_date = 'onclick="closeInput(\'' . $id_date . '\')"'; - $close_editor_date = ''; - $submit_js_date = 'onclick="submitDate(\'' . $id_date . '\')"'; - $submit_date = ''; - $date_buttons = '
    ' . $modify_date . $close_editor_date . $submit_date . '
    '; - - // mode nouvel article - if(self::$new_article_mode){ - $delete_article = ''; - // valider la création d'un nouvel article - $submit_js = 'onclick="submitArticle(\'' . $_GET['id'] . '\')"'; - $submit_article = '

    ' . "\n"; - } - // mode article existant - else{ - $url = new URL(['action' => 'delete_article', 'id' => $_GET['id'], 'from' => $_GET['from'] ?? '']); - $delete_article = '
    -

    - Supprimer l\'article -

    ' . "\n"; // this.closest('form').submit() = submit du formulaire avec javascript - $submit_article = ''; - } - - $admin_buttons = $share_button . $delete_article . $submit_article . $from_to_button; - } - // autre page - else{ - $delete_js = 'onclick="deleteArticle(\'' . $id . '\')"'; - $delete_article = '

    ' . "\n"; - - $close_editor = ''; - $submit_article = ''; - - $admin_buttons = $from_to_button . $share_button . $delete_article . $close_editor . $submit_article; - } - } - else{ - $admin_buttons = $share_button . $from_to_button; - } - - ob_start(); - require($viewFile); - $this->html .= ob_get_clean(); - } - } -} diff --git a/src/view/NewPageBuilder.php b/src/view/NewPageBuilder.php deleted file mode 100644 index 0a3a137..0000000 --- a/src/view/NewPageBuilder.php +++ /dev/null @@ -1,49 +0,0 @@ -getName() . '.php'; - - if(IS_ADMIN && file_exists($viewFile)) - { - /*if(!empty($node->getNodeData()->getData())) - { - extract($node->getNodeData()->getData()); - }*/ - - $this->unfoldOptions(Model::$menu); - - ob_start(); - require $viewFile; // insertion de $this->html généré par unfoldMenu - $this->html = ob_get_clean(); // pas de concaténation .= cette fois on écrase - } - else{ - header('Location: ' . new URL); - die; - } - } - - private function unfoldOptions(Page $page): void - { - foreach($page->getChildren() as $entry){ - $this->options .= '\n"; - if(count($entry->getChildren()) > 0){ - $this->unfoldOptions($entry); - } - } - } -} \ No newline at end of file diff --git a/src/view/NewsBlockBuilder.php b/src/view/NewsBlockBuilder.php deleted file mode 100644 index f1fe12b..0000000 --- a/src/view/NewsBlockBuilder.php +++ /dev/null @@ -1,73 +0,0 @@ -getName() . '.php'; // = news_block.php, actuellement identique à post_block.php - - if(file_exists($viewFile)) - { - if(!empty($node->getNodeData()->getData())) - { - extract($node->getNodeData()->getData()); - } - - // stratégie d'affichage du contenu (utilisation de méthodes ou de classe List, GridPresentation, etc) - $section_class = $node->getNodeData()->getPresentation(); // = list, grid , mosaic ou carousel - $cols_min_width = ''; - if($section_class === 'grid'){ - $min_width = $node->getNodeData()->getColsMinWidth(); - $cols_min_width = 'grid-template-columns: repeat(auto-fit, minmax(' . (string)$min_width . 'px, 1fr));'; - } - $fetch_button_hidden = $node->getNodeData()->getNumberOfPages() > 1 ? '' : ' hidden'; - - // ajouter un article - $new_article = ''; - if(IS_ADMIN) - { - $id = 'n' . $this->id_node; - - $share_button = ''; - - $new_button = '

    '; - - $modify_js = 'onclick="openEditor(\'' . $id . '\')"'; - $modify_article = '' . "\n"; - - $delete_js = 'onclick="deleteArticle(\'' . $id . '\')"'; - $delete_article = '' . "\n"; - - $close_js = 'onclick="closeEditor(\'' . $id . '\')"'; - $close_editor = ''; - - $submit_js = 'onclick="submitArticle(\'' . $id . '\', clone' . $this->id_node . ')"'; - $submit_article = ''; - - $html = ''; - $admin_buttons = $new_button . $modify_article . $delete_article . $close_editor . $submit_article; - - // insérer post.php pour le bouton "Nouvel article", new.php pour les vrais articles - ob_start(); - require self::VIEWS_PATH . 'post.php'; // nécéssite $admin_buttons et $html - $new_article = ob_get_clean(); - } - - // articles existants - $this->useChildrenBuilder($node); - $content = $this->html; - - ob_start(); - require $viewFile; // utilise $content - $this->html = ob_get_clean(); // nouveau contenu - } - } -} \ No newline at end of file diff --git a/src/view/PostBlockBuilder.php b/src/view/PostBlockBuilder.php deleted file mode 100644 index c6094e8..0000000 --- a/src/view/PostBlockBuilder.php +++ /dev/null @@ -1,88 +0,0 @@ -getName() . '.php'; // = post_block.php, actuellement identique à news_block.php - - if(file_exists($viewFile)) - { - if(!empty($node->getNodeData()->getData())) - { - extract($node->getNodeData()->getData()); - } - - // stratégie d'affichage du contenu (utilisation de méthodes ou de classe List, GridPresentation, etc) - $section_class = $node->getNodeData()->getPresentation(); // = list, grid , mosaic ou carousel - $cols_min_width = ''; - if($section_class === 'grid'){ - $min_width = $node->getNodeData()->getColsMinWidth(); - $cols_min_width = 'grid-template-columns: repeat(auto-fit, minmax(' . (string)$min_width . 'px, 1fr));'; - } - $fetch_button_hidden = $node->getNodeData()->getNumberOfPages() > 1 ? '' : ' hidden'; - - // ajouter un article - // => fait un peu double emploi avec PostBuilder - $new_article = ''; - if(IS_ADMIN) - { - $id = 'n' . $this->id_node; - - $share_button = ''; - - $new_button = '

    ' . "\n" . '

    '; - - $modify_js = 'onclick="openEditor(\'' . $id . '\')"'; - $modify_article = '' . "\n"; - - $up_js = 'onclick="switchPositions(\'' . $id . '\', \'up\')"'; - $up_button = '' . "\n"; - - $down_js = 'onclick="switchPositions(\'' . $id . '\', \'down\')"'; - $down_button = '' . "\n"; - - $delete_js = 'onclick="deleteArticle(\'' . $id . '\')"'; - $delete_article = '' . "\n"; - - $close_js = 'onclick="closeEditor(\'' . $id . '\')"'; - $close_editor = ''; - - $position = ''; - - $submit_js = 'onclick="submitArticle(\'' . $id . '\', clone' . $this->id_node . ')"'; - $submit_article = ''; - - $html = ''; - $admin_buttons = $new_button . $modify_article . $up_button . $down_button . $delete_article . $close_editor . $submit_article . $position; - - // squelette d'un nouvel article - ob_start(); - require self::VIEWS_PATH . 'post.php'; - $new_article = ob_get_clean(); - } - - // articles existants - $this->useChildrenBuilder($node); - $content = $this->html; - - ob_start(); - require $viewFile; - $this->html = ob_get_clean(); // pas de concaténation ici, on écrase - } - } -} \ No newline at end of file diff --git a/src/view/PostBuilder.php b/src/view/PostBuilder.php deleted file mode 100644 index e0bf985..0000000 --- a/src/view/PostBuilder.php +++ /dev/null @@ -1,57 +0,0 @@ -getName() . '.php'; - - if(file_exists($viewFile)) - { - // html - $title = $node->getArticle()->getTitle(); - $html = $node->getArticle()->getContent(); - $id = (string)$node->getId(); - - // partage - $share_link = new URL(['page' => CURRENT_PAGE], $id); - $share_js = 'onclick="copyInClipBoard(\'' . $share_link . '\')"'; - $share_button = '' . "\n"; - - // modifier un article - // => fait un peu double emploi avec PostBlockBuilder - $admin_buttons = ''; - if(IS_ADMIN) - { - $modify_js = 'onclick="openEditor(\'' . $id . '\')"'; - $modify_article = '

    ' . "\n"; - - $up_js = 'onclick="switchPositions(\'' . $id . '\', \'up\')"'; - $up_button = '

    ' . "\n"; - - $down_js = 'onclick="switchPositions(\'' . $id . '\', \'down\')"'; - $down_button = '

    ' . "\n"; - - $delete_js = 'onclick="deleteArticle(\'' . $id . '\')"'; - $delete_article = '

    ' . "\n"; - - $close_js = 'onclick="closeEditor(\'' . $id . '\')"'; - $close_editor = ''; - - $submit_js = 'onclick="submitArticle(\'' . $id . '\')"'; - $submit_article = ''; - - $admin_buttons = $modify_article . $up_button . $down_button . $delete_article . $close_editor . $submit_article; - } - - ob_start(); - require($viewFile); - $this->html .= ob_get_clean(); - } - } -} diff --git a/src/view/ShowEmailsBuilder.php b/src/view/ShowEmailsBuilder.php deleted file mode 100644 index e88b349..0000000 --- a/src/view/ShowEmailsBuilder.php +++ /dev/null @@ -1,69 +0,0 @@ -getName() . '.php'; - if(file_exists($viewFile)) - { - // objets Email groupés par destinataire - $emails_by_recipient = []; - foreach($node->getNodeData()->getEmails() as $email){ - $recipient = $email->getRecipient(); - $emails_by_recipient[$recipient][] = $email; - } - - // affiche une table par destinataire - $emails = ''; - foreach($emails_by_recipient as $recipient => $emails_list){ - $html = '

    Destinataire: ' . $recipient . '

    - - - - - - - - - - - - - '; - - // insère les données - foreach($emails_list as $email){ - $html .= ' - - - - - - - - '; - } - - $html .= ' -
    ExpéditeurAdresseContenuDateEffacement prévu leSensible
    ' . htmlspecialchars($email->getSenderName()) . '' . htmlspecialchars($email->getSenderAddress()) . '' . htmlspecialchars($email->getContent()) . '' . $email->getDateTime()->format('d/m/Y') . '' . $email->getDeletionDate()->format('d/m/Y') . 'isSensitive() ? 'checked' : '') . ' onclick="toggleSensitiveEmail(' . $email->getId() . ')">
    '; - $emails .= $html; - } - - ob_start(); - require $viewFile; // insertion de $this->html généré par unfoldMenu - $this->html = ob_get_clean(); // pas de concaténation .= cette fois on écrase - } - else{ - header('Location: ' . new URL(['error' => 'show_emails_view_not_found'])); - die; - } - } -} \ No newline at end of file diff --git a/src/view/UserEditBuilder.php b/src/view/UserEditBuilder.php deleted file mode 100644 index 0a347d4..0000000 --- a/src/view/UserEditBuilder.php +++ /dev/null @@ -1,62 +0,0 @@ -getName() . '.php'; - - $error_messages = [ - 'error_non_valid_captcha' => 'Erreur au test anti-robot, veuillez saisir un nombre entier.', - 'captcha_server_error' => 'captcha_server_error', - - 'bad_login_or_password' => 'Mauvais identifiant ou mot de passe, veuillez réessayer.', // ne pas indiquer où est l'erreur - 'bad_solution_captcha' => 'Erreur au test anti-robot, veuillez réessayer.', - 'forbidden_characters' => 'Caractères interdits: espaces, tabulations, sauts CR/LF.', - 'same_username_as_before' => 'Nouveau nom identique au précédent.', - 'same_password_as_before' => 'Nouveau mot de passe identique au précédent.' - ]; - - $error_username = isset($_GET['error_username']) ? $error_messages[$_GET['error_username']] : ''; - $success_username = (isset($_GET['success_username']) && $_GET['success_username']) ? 'Identifiant modifié avec succès.' : ''; - $error_password = isset($_GET['error_password']) ? $error_messages[$_GET['error_password']] : ''; - $success_password = (isset($_GET['success_password']) && $_GET['success_password']) ? 'Mot de passe modifié avec succès.' : ''; - - $captcha = new Captcha; - $_SESSION['captcha'] = $captcha->getSolution(); // enregistrement de la réponse du captcha pour vérification - - $link_user_form = new URL(['action' => 'update_username']); - isset($_GET['from']) ? $link_user_form->addParams(['from' => $_GET['from']]) : ''; - isset($_GET['id']) ? $link_user_form->addParams(['id' => $_GET['id']]) : ''; - - $link_password_form = new URL(['action' => 'update_password']); - isset($_GET['from']) ? $link_password_form->addParams(['from' => $_GET['from']]) : ''; - isset($_GET['id']) ? $link_password_form->addParams(['id' => $_GET['id']]) : ''; - - $link_exit = new URL; - isset($_GET['from']) ? $link_exit->addParams(['page' => $_GET['from'] ]) : ''; - isset($_GET['id']) ? $link_exit->addParams(['id' => $_GET['id']]) : ''; - - $link_logout = new URL(['action' => 'deconnection']); - isset($_GET['from']) ? $link_logout->addParams(['from' => $_GET['from'] ]) : ''; - isset($_GET['id']) ? $link_logout->addParams(['id' => $_GET['id']]) : ''; - - ob_start(); - require $viewFile; - $this->html = ob_get_clean(); // nouveau contenu - } -} \ No newline at end of file diff --git a/src/view/ViewDirector.php b/src/view/ViewDirector.php deleted file mode 100644 index c1a38b8..0000000 --- a/src/view/ViewDirector.php +++ /dev/null @@ -1,28 +0,0 @@ -root_node = $root_node; - $this->useChildrenBuilder($this->root_node); - - if(isset($_SESSION['flash_message'])){ - $this->html .= ''; - unset($_SESSION['flash_message']); - } - - return $this->html; - } -} \ No newline at end of file diff --git a/src/view/templates/calendar.php b/src/view/templates/calendar.php deleted file mode 100644 index 18f68f2..0000000 --- a/src/view/templates/calendar.php +++ /dev/null @@ -1,11 +0,0 @@ - -
    - - - -

    -
    -
    - -
    -
    \ No newline at end of file diff --git a/src/view/templates/footer.php b/src/view/templates/footer.php deleted file mode 100644 index 6450e21..0000000 --- a/src/view/templates/footer.php +++ /dev/null @@ -1,40 +0,0 @@ - -
    - -
    -
    - - - -
    - - - - -
    -
    -
    - -
    -
    -
    -
    - - \ No newline at end of file diff --git a/src/view/templates/form.php b/src/view/templates/form.php deleted file mode 100644 index 384bde9..0000000 --- a/src/view/templates/form.php +++ /dev/null @@ -1,36 +0,0 @@ - -
    -

    - -
    - - - - - - - - - -
    - -
    - - -
    - -
    - - - - -
    - -
    - -

    -
    -

    - Une copie de votre e-mail (nom, adresse et message) sera conservée dans notre base de données dans le but de pouvoir mieux répondre à votre demande et éventuellement dans d'autres buts (prospection). Ces données seront traitées automatiquement par notre serveur et conservées pendant au maximum mois à compter de votre dernier message. Ce traitement repose sur votre consentement. Vous pouvez consulter, modifier ou supprimer vos données sur simple demande. -

    -
    \ No newline at end of file diff --git a/src/view/templates/form_admin.php b/src/view/templates/form_admin.php deleted file mode 100644 index d745656..0000000 --- a/src/view/templates/form_admin.php +++ /dev/null @@ -1,72 +0,0 @@ - -
    -

    - - onclick="keepEmails(getNodeData()->getId() ?>)"> -

    -

    Notez que ces enregistrements sont des données personnelles et sont concernés par le RGPD.

    -

    -

    - - -

    -

    - - -

    - -
    -
    -

    Paramètres d'envoi

    -

    - - - - -

    -

    - - - -

    -

    - - - - -

    -

    - - - - -

    -

    Il s'agit du service qui acheminera les messages envoyés par ce formulaire.
    - Site web => serveur d'envoi => serveur de réception => votre application "mail"

    -

    - Ce formulaire utilise les paramètres dans le fichier config.ini sur le serveur à mois que vous n'en renseignez d'autres ci-dessus. Les adresses d'envoi et de réception peuvent être identiques. -

    -

    Le site web peut ne pas réussir à se connecter à certains fournisseurs.

    -
    -
    -

    Paramètres de réception

    -

    - - - - -

    -
    -
    -

    -

    Vérifie la connexion au serveur d'envoi. Pour tester la réception, consultez vos e-mails à l'adresse de réception.

    -

    -
    \ No newline at end of file diff --git a/src/view/templates/galery.php b/src/view/templates/galery.php deleted file mode 100644 index 97f8cd8..0000000 --- a/src/view/templates/galery.php +++ /dev/null @@ -1,12 +0,0 @@ - -
    -

    - - -
    - -
    - -
    \ No newline at end of file diff --git a/src/view/templates/head.php b/src/view/templates/head.php deleted file mode 100644 index bce0a19..0000000 --- a/src/view/templates/head.php +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - - <?= $title ?> - - - - - - \ No newline at end of file diff --git a/src/view/templates/header.php b/src/view/templates/header.php deleted file mode 100644 index 400c7c0..0000000 --- a/src/view/templates/header.php +++ /dev/null @@ -1,55 +0,0 @@ - - -
    -
    - -
    -
    - -
    -
    - -
    -
    -
    -
    - -
    -
    -

    - - -

    -

    - - -

    -
    -
    -
    -
    - -
    -
    - -
    -
    - - - -
    \ No newline at end of file diff --git a/src/view/templates/login.php b/src/view/templates/login.php deleted file mode 100644 index 3257690..0000000 --- a/src/view/templates/login.php +++ /dev/null @@ -1,31 +0,0 @@ - -
    -

    Connexion à l'espace d'administration

    -
    -

    Veuillez saisir votre identifiant et votre mot de passe.

    -

    -
    -

    -

    -

    -

    - - -

    Montrez que vous n'êtes pas un robot.
    - - -

    - - -
    -

    Au fait? Vous n'utilisez pas votre propre ordinateur ou téléphone?
    - Utilisez la navigation privée.

    -
    -
    -
    - -
    \ No newline at end of file diff --git a/src/view/templates/maintenance.php b/src/view/templates/maintenance.php deleted file mode 100644 index 61d5146..0000000 --- a/src/view/templates/maintenance.php +++ /dev/null @@ -1,62 +0,0 @@ - -
    -

    Maintenance du site

    -
    -

    -
    - Qui a essayé de se connecter, quand et a-t'il réussi? -

    -
    -

    - -
    -
    -

    - - reçus depuis tous les formulaires de contact -

    -
    -
    -

    - - -
    - Obtenir un fichier SQL à conserver. Une sauvegarde (désignée par "auto") est réalisée à chaque connexion au mode admin. -

    -
    -
    -

    Restaurer la base de données à partir d'un fichier SQL.
    - Attention, l'actuelle BDD sera écrasée! (à l'exception de la table user) -

    -
    -
    - - - -
    -
    -
    - - - -
    -
    -
    -

    - - -
    - Toutes vos photos et vos documents dans un .zip -

    -

    -
    - Gérer les fichiers multimedia, fonction actuellement indisponible -

    -
    - - -
    \ No newline at end of file diff --git a/src/view/templates/menu.php b/src/view/templates/menu.php deleted file mode 100644 index ccfb518..0000000 --- a/src/view/templates/menu.php +++ /dev/null @@ -1,48 +0,0 @@ - - -
    - -
    \ No newline at end of file diff --git a/src/view/templates/modify_block.php b/src/view/templates/modify_block.php deleted file mode 100644 index e966ee3..0000000 --- a/src/view/templates/modify_block.php +++ /dev/null @@ -1,58 +0,0 @@ - -
    -
    - -

    - - -

    -
    -

    - - -

    -
    - - - -
    -
    -
    -getName() === 'news_block'){ -?> -
    - - -
    -getName())){ -?> -
    -
    - - -
    -
    - - pixels -
    -
    -
    - -
    - -
    \ No newline at end of file diff --git a/src/view/templates/modify_page.php b/src/view/templates/modify_page.php deleted file mode 100644 index d32179c..0000000 --- a/src/view/templates/modify_page.php +++ /dev/null @@ -1,58 +0,0 @@ - -
    -

    Modification de la page

    -
    -
    -

    - - - -

    -
    - - - - -
    -
    -
    - -
    - - -
    -
    -
    -
    -
    - - - - -
    -
    -
    -
    -

    Ajouter un bloc de page

    -
    -

    -

    -

    - - -

    -
    -
    -

    Articles libres: textes riches générés par l'éditeur et librement positionnables

    -

    Actualités: contenus structurés avec titre, aperçu, date et possédant une page dédiée

    - -
    -
    -
    -

    Modifier un bloc

    - -
    -
    -
    \ No newline at end of file diff --git a/src/view/templates/new.php b/src/view/templates/new.php deleted file mode 100644 index 30b244f..0000000 --- a/src/view/templates/new.php +++ /dev/null @@ -1,27 +0,0 @@ - -
    -
    -
    - -
    - -
    - -
    - -
    - -
    - -
    -

    - - Chargement... -

    -
    - -
    - -
    -
    -
    \ No newline at end of file diff --git a/src/view/templates/new_page.php b/src/view/templates/new_page.php deleted file mode 100644 index 7bf4c01..0000000 --- a/src/view/templates/new_page.php +++ /dev/null @@ -1,35 +0,0 @@ - -
    -

    Création d'une nouvelle page

    -
    -
    -

    - - -

    -

    - - -

    -

    - - -

    - -

    - - -

    - -
    -
    -
    -
    - -
    \ No newline at end of file diff --git a/src/view/templates/news_block.php b/src/view/templates/news_block.php deleted file mode 100644 index 43dbe01..0000000 --- a/src/view/templates/news_block.php +++ /dev/null @@ -1,14 +0,0 @@ - -
    -

    - - -
    - -
    -
    - -
    -
    \ No newline at end of file diff --git a/src/view/templates/post.php b/src/view/templates/post.php deleted file mode 100644 index d0d01b2..0000000 --- a/src/view/templates/post.php +++ /dev/null @@ -1,14 +0,0 @@ - -
    -
    -
    - -
    -
    - -
    -
    - -
    -
    -
    \ No newline at end of file diff --git a/src/view/templates/post_block.php b/src/view/templates/post_block.php deleted file mode 100644 index 43dbe01..0000000 --- a/src/view/templates/post_block.php +++ /dev/null @@ -1,14 +0,0 @@ - -
    -

    - - -
    - -
    -
    - -
    -
    \ No newline at end of file diff --git a/src/view/templates/show_emails.php b/src/view/templates/show_emails.php deleted file mode 100644 index f561dc7..0000000 --- a/src/view/templates/show_emails.php +++ /dev/null @@ -1,30 +0,0 @@ - -
    -

    Table "email" de la base de données

    -

    - Les e-mails ci-dessous sont des copies de ceux arrivés dans votre boite de messagerie depuis tous les formulaires existant sur le site. Ils sont conservés dans un but pratique (historique, prospection) ou dans tout autre but justifiant leur conservation. Cette conservation est optionnelle et règlementée par la loi européenne RGPD.
    - Notez qu'utiliser cette application ne vous garantit pas d'être en conformité avec la loi, c'est un outil vous permettant de faire les choses comme vous l'entendez. Si tout ceci vous gêne, désactivez le stockage des e-mails et supprimez ceux déjà enregistrés (ci-dessous). -

    -

    Durées de conservation

    -

    - Ce sont des durées maximales, les données peuvent être supprimées plus tôt ou même immédiatement. Le faire est d'ailleurs une obligation dans le cas où les personnes concernées le demandent.
    -

    -

    - Les e-mails ordinaires d'un même expéditeur (même adresse e-mail) sont tous supprimés simultanément lorsque le plus récent d'entre eux atteint les 3 ans (utilisateur est considéré inactif).
    - Les e-mails sensibles quand à eux sont supprimés 5 ans après être devenus sensibles (durée juridique d'une preuve). -

    -

    - Un nettoyeur supprimant les messages dépassant ces durées est exécuté au moment de votre connexion au mode administrateur.
    - Si vos connexions sont rares, il est possible d'automatiser ce nettoyage à l'aide d'une tâche CRON. Pour cela, vous devez configurer le serveur pour qu'il exécute periodiquement la commande "php /chemin/du/site/bin/cron.php". -

    -

    Données sensibles

    -

    - Un e-mail peut-être considéré comme "sensible". Vous pouvez rendre un e-mail sensible lorsqu'il possède une valeur de preuve dans le cas d'un litige.
    - Lorsqu'une personne demande la suppression de ses données personnelles du serveur, les e-mails sensibles peuvent être conservés, vous aurez noté que la durée de conservation est calculée différement. -

    -

    - Les spams ne sont pas sensibles, c'est juste de la pollution, supprimez-les! -

    - - -
    \ No newline at end of file diff --git a/src/view/templates/user_create.php b/src/view/templates/user_create.php deleted file mode 100644 index 870b904..0000000 --- a/src/view/templates/user_create.php +++ /dev/null @@ -1,53 +0,0 @@ - 'Erreur au test anti-robot, veuillez saisir un nombre entier.', - 'captcha_server_error' => 'captcha_server_error', - 'bad_solution_captcha' => 'Erreur au test anti-robot, veuillez réessayer.', - 'bad_login_or_password' => 'Mauvais identifiant ou mot de passe, veuillez réessayer.', - 'different_passwords' => 'Les deux mots de passe saisis sont différents', - 'forbidden_characters' => 'Caractères interdits: espaces, tabulations, sauts CR/LF.' -]; -$error = isset($_GET['error']) ? $error_messages[$_GET['error']] : ''; - -$captcha = new Captcha; -$_SESSION['captcha'] = $captcha->getSolution(); // enregistrement de la réponse du captcha pour vérification -?> - - - - - Bienvenue - - - - - -
    -
    -

    Bienvenue.

    -

    Pour commencer, veuillez créer un compte administrateur.

    -
    -

    -
    -

    -

    -

    -

    -

    -

    - - -

    Montrez que vous n'êtes pas un robot.
    - - -

    - - -
    -
    -
    -
    - - \ No newline at end of file diff --git a/src/view/templates/user_edit.php b/src/view/templates/user_edit.php deleted file mode 100644 index 9191e7d..0000000 --- a/src/view/templates/user_edit.php +++ /dev/null @@ -1,63 +0,0 @@ - -
    -
    -
    -

    Mon compte

    -
    - -
    -
    -
    -
    -
    -

    Modifier mon nom d'utilisateur.

    -

    -

    -
    -

    -

    -

    -

    -

    -

    - - -

    Montrez que vous n'êtes pas un robot.
    - - -

    - - -
    -
    -
    -

    Modifier mon mot de passe.

    -

    -

    -
    -

    -

    -

    -

    -

    -

    - - -

    Montrez que vous n'êtes pas un robot.
    - - -

    - - -
    -
    -
    -
    -
    - -
    \ No newline at end of file -- cgit v1.2.3