From 3b369122645b07b290f7fcc7bccb4787745cd5ea Mon Sep 17 00:00:00 2001
From: polo
Date: Tue, 24 Mar 2026 22:39:29 +0100
Subject: =?UTF-8?q?mode=20maintenance,=20optimisation=20moins=20de=20contr?=
=?UTF-8?q?=C3=B4les=20en=20mode=20run,=20dossier=20service=20et=20d=C3=A9?=
=?UTF-8?q?placement=20fichiers,=20sessions=20et=20entit=C3=A9=20User=20pr?=
=?UTF-8?q?=C3=A9par=C3=A9es=20=C3=A0=20l'impl=C3=A9mentation=20hypoth?=
=?UTF-8?q?=C3=A9tique=20des=20r=C3=B4les,=20entit=C3=A9=20AppMetadata,=20?=
=?UTF-8?q?meilleure=20s=C3=A9curit=C3=A9=20de=20fillStartingDatabase?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/Captcha.php | 55 ------
src/Config.php | 86 ---------
src/EmailService.php | 102 ----------
src/FormValidation.php | 215 ---------------------
src/Security.php | 110 -----------
src/URL.php | 88 ---------
src/controller/UserController.php | 65 +++----
src/controller/ViewController.php | 6 +-
src/installation.php | 161 ----------------
src/model/Menu.php | 4 -
src/model/entities/AppMetadata.php | 46 +++++
src/model/entities/User.php | 19 +-
src/router.php | 384 -------------------------------------
src/service/AppMode.php | 56 ++++++
src/service/Captcha.php | 55 ++++++
src/service/Config.php | 86 +++++++++
src/service/EmailService.php | 102 ++++++++++
src/service/FormValidation.php | 215 +++++++++++++++++++++
src/service/Installation.php | 209 ++++++++++++++++++++
src/service/Security.php | 110 +++++++++++
src/service/URL.php | 88 +++++++++
src/service/router.php | 384 +++++++++++++++++++++++++++++++++++++
src/service/session.php | 86 +++++++++
src/view/CalendarBuilder.php | 2 +-
src/view/FooterBuilder.php | 2 +-
src/view/FormBuilder.php | 9 +-
src/view/GaleryBuilder.php | 2 +-
src/view/HeadBuilder.php | 2 +-
src/view/HeaderBuilder.php | 2 +-
src/view/LoginBuilder.php | 2 +-
src/view/MainBuilder.php | 2 +-
src/view/MenuBuilder.php | 2 +-
src/view/NewBuilder.php | 2 +-
src/view/NewPageBuilder.php | 2 +-
src/view/NewsBlockBuilder.php | 2 +-
src/view/PostBlockBuilder.php | 2 +-
src/view/PostBuilder.php | 2 +-
src/view/UserEditBuilder.php | 2 +-
src/view/templates/footer.php | 2 +-
src/view/templates/header.php | 2 +-
src/view/templates/maintenance.php | 27 +++
src/view/templates/user_create.php | 2 +-
src/view/templates/user_edit.php | 2 +-
43 files changed, 1537 insertions(+), 1267 deletions(-)
delete mode 100644 src/Captcha.php
delete mode 100644 src/Config.php
delete mode 100644 src/EmailService.php
delete mode 100644 src/FormValidation.php
delete mode 100644 src/Security.php
delete mode 100644 src/URL.php
delete mode 100644 src/installation.php
create mode 100644 src/model/entities/AppMetadata.php
delete mode 100644 src/router.php
create mode 100644 src/service/AppMode.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/Security.php
create mode 100644 src/service/URL.php
create mode 100644 src/service/router.php
create mode 100644 src/service/session.php
create mode 100644 src/view/templates/maintenance.php
(limited to 'src')
diff --git a/src/Captcha.php b/src/Captcha.php
deleted file mode 100644
index a0c7a54..0000000
--- a/src/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/Config.php b/src/Config.php
deleted file mode 100644
index 76b34e2..0000000
--- a/src/Config.php
+++ /dev/null
@@ -1,86 +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/EmailService.php b/src/EmailService.php
deleted file mode 100644
index c6d97b4..0000000
--- a/src/EmailService.php
+++ /dev/null
@@ -1,102 +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; // une adresse bidon est donnée à setFrom()
- $email_from_name = $form_data->getData()['email_from_name'] ?? Config::$email_from_name; // = site web
- $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, Config::$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} ]+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/URL.php b/src/URL.php
deleted file mode 100644
index a6d6379..0000000
--- a/src/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/controller/UserController.php b/src/controller/UserController.php
index f3c99e7..6928e86 100644
--- a/src/controller/UserController.php
+++ b/src/controller/UserController.php
@@ -22,14 +22,12 @@ class UserController
// account
static public function existUsers(EntityManager $entityManager): bool
{
- $nb_users = $entityManager
- ->createQuery('SELECT COUNT(u.id_user) FROM App\Entity\User u')
- ->getSingleScalarResult();
-
- if($nb_users === 0) // table vide
+ if(!$entityManager // table vide
+ ->createQuery("SELECT u FROM App\Entity\User u")
+ ->setMaxResults(1)
+ ->getOneOrNullResult())
{
- $_SESSION['user'] = '';
- $_SESSION['admin'] = false;
+ unset($_SESSION['user']);
return false;
}
else{
@@ -38,7 +36,7 @@ class UserController
}
// account
- static public function createUser(EntityManager $entityManager)
+ static public function createAdminUser(EntityManager $entityManager)
{
unset($_SESSION['user']);
@@ -48,7 +46,7 @@ class UserController
$error = '';
if($form->validate()){
$password = password_hash($_POST['password'], PASSWORD_DEFAULT);
- $user = new App\Entity\User($_POST['login'], $password);
+ $user = new User($_POST['login'], 'admin', $password);
$entityManager->persist($user);
$entityManager->flush();
}
@@ -64,31 +62,34 @@ class UserController
die;
}
+ // account
+ //static public function createUser(EntityManager $entityManager){}
+
// auth
static public function connect(EntityManager $entityManager): void
{
- if($_SESSION['admin']) // déjà connecté?
+ if(IS_ADMIN) // déjà connecté?
{
header('Location: ' . new URL);
die;
}
- $_SESSION['user'] = '';
- $_SESSION['admin'] = false;
+ unset($_SESSION['user']);
$form = new FormValidation($_POST, 'connection');
$error = '';
if($form->validate()){
// à mettre dans une classe métier UserService, Authentication, AuthService?
- $user = self::getUser($_POST['login'], $entityManager);
+ $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'] = $_POST['login'];
- $_SESSION['admin'] = true;
+ $_SESSION['user']['id'] = $user->getId();
+ $_SESSION['user']['username'] = $user->getLogin();
+ $_SESSION['user']['role'] = $user->getRole();
EmailService::cleanEmails($entityManager);
@@ -123,7 +124,7 @@ class UserController
static public function disconnect(): void
{
// nettoyage complet
- $_SESSION = []; // mémoire vive
+ unset($_SESSION['user']); // mémoire vive
session_destroy(); // fichier côté serveur
setcookie('PHPSESSID', '', time() - 86400, '/'); // cookie de session
@@ -138,7 +139,7 @@ class UserController
// user
static public function updateUsername(EntityManager $entityManager): void
{
- if(!$_SESSION['admin']){ // superflux, fait dans le routeur
+ if(!IS_ADMIN){ // superflux, fait dans le routeur
self::disconnect();
}
@@ -150,11 +151,11 @@ class UserController
$error = '';
if($form->validate()){
// à mettre dans une classe métier UserService?
- $user = self::getUser($_POST['login'], $entityManager);
+ $user = self::getUserByName($_POST['login'], $entityManager);
if(password_verify($_POST['password'], $user->getPassword())){
$user->setLogin($_POST['new_login']);
$entityManager->flush();
- $_SESSION['user'] = $_POST['new_login'];
+ $_SESSION['user']['username'] = $_POST['new_login'];
$url->addParams(['success_username' => 'new_login']);
$error = '';
@@ -178,7 +179,7 @@ class UserController
// user
static public function updatePassword(EntityManager $entityManager): void
{
- if(!$_SESSION['admin']){ // superflux, fait dans le routeur
+ if(!IS_ADMIN){ // superflux, fait dans le routeur
self::disconnect();
}
@@ -190,7 +191,7 @@ class UserController
$error = '';
if($form->validate()){
// à mettre dans une classe métier UserService?
- $user = self::getUser($_POST['login'], $entityManager);
+ $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);
@@ -216,26 +217,22 @@ class UserController
}
// dans une classe mère ou un trait après découpage de UserController?
- static private function getUser(string $login, EntityManager $entityManager): ?User
+ static private function getUserByName(string $login, EntityManager $entityManager): ?User
{
- $users = $entityManager->getRepository('App\Entity\User')->findBy(['login' => $login]);
-
- if(count($users) === 0)
- {
- $_SESSION['user'] = '';
- $_SESSION['admin'] = false;
- }
-
- foreach($users as $user)
- {
- if($user->getLogin() === $login)
- {
+ $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
diff --git a/src/controller/ViewController.php b/src/controller/ViewController.php
index 8c95526..cf3477c 100644
--- a/src/controller/ViewController.php
+++ b/src/controller/ViewController.php
@@ -21,7 +21,7 @@ class ViewController extends AbstractBuilder // ViewController est aussi le prem
/* 1/ 1er contrôle des paramètres */
// mode modification d'une page
- if($_SESSION['admin']
+ if(IS_ADMIN
&& $request->query->has('mode') && $request->query->get('mode') === 'page_modif'
&& !in_array(CURRENT_PAGE, ['article', 'new_page', 'menu_paths', 'user_edit', 'connection']))
{
@@ -29,7 +29,7 @@ class ViewController extends AbstractBuilder // ViewController est aussi le prem
}
// page article: mode création et erreurs d'id
if(CURRENT_PAGE === 'article'){
- if($_SESSION['admin']){
+ if(IS_ADMIN){
if(!$request->query->has('id')){
return new Response($this->html, 302);
}
@@ -58,7 +58,7 @@ class ViewController extends AbstractBuilder // ViewController est aussi le prem
/* 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' && !$_SESSION['admin'] && self::$root_node->getNodeByName('main')->getAdoptedChild() === null){
+ if(CURRENT_PAGE === 'article' && !IS_ADMIN && self::$root_node->getNodeByName('main')->getAdoptedChild() === null){
return new Response($this->html, 302);
}
diff --git a/src/installation.php b/src/installation.php
deleted file mode 100644
index 2bef8e5..0000000
--- a/src/installation.php
+++ /dev/null
@@ -1,161 +0,0 @@
-l'extension " . $extension . ' 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;
- }
- 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;
- }
-}
-
-function installation(): void
-{
- // -- droits des fichiers et dossiers --
- $droits_dossiers = 0700;
- $droits_fichiers = 0600;
-
- if(!file_exists('user_data')){
- // créer le dossier user_data
- mkdir('user_data/');
- chmod('user_data/', $droits_dossiers);
- 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
- Aide: "serveur web" se nomme "www-data" sur debian et ubuntu, il s\'appelera "http" sur d\'autres distributions.
';
- die;
- }
-
- if(!file_exists('../config/config.ini')){
- // aide à la création du config.ini
- echo 'Ce fichier contient les codes de la base de données et quelques paramètres utilisés pour créer les liens internes.
- Un modèle est disponible, il s\'agit du fichier config/config-template.ini
- Quand vous aurez terminé votre config.ini, donnez-lui si possible des droits 600.
';
- die;
- }
-}
-
-/*
- // droits du config.ini
- if(substr(sprintf('%o', fileperms('../config/config.ini')), -4) != 600){
- chmod('../config/config.ini', $droits_fichiers);
- }
-
- // création de data et sous-dossiers
- if(!file_exists('../data')){
- mkdir('../data/');
- chmod('../data/', $droits_dossiers);
- }
- if(!touch('../data')){
- echo("dossier data non autorisé en écriture");
- die;
- }
- $sous_dossiers = array('images', 'images-mini', 'videos');
- foreach ($sous_dossiers as $sous_dossier){
- if(!file_exists('../data/' . $sous_dossier)){
- mkdir('../data/' . $sous_dossier);
- chmod('../data/' . $sous_dossier, $droits_dossiers);
- }
- if(!touch('../data/' . $sous_dossier)){
- echo("dossier data non autorisé en écriture");
- die;
- }
- }
-*/
-
-// création d'un site minimal avec une page d'accueil à la toute 1ère visite du site
-// fonctiona appelée après la première requête envoyée en BDD,
-// en l'occurence dans Menu parce que count($bulk_data) === 0
-function fillStartingDatabase(EntityManager $entityManager){
- /* -- 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');
-
- /* -- 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);
-
- /* -- 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);
-
- /* -- 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);
-
- /* -- table node_data -- */
- $entityManager->persist($head_data);
- $entityManager->persist($header_data);
- $entityManager->persist($footer_data);
- $entityManager->persist($emails_data);
-
- $entityManager->flush();
- header('Location: ' . new URL);
- die;
-}
\ No newline at end of file
diff --git a/src/model/Menu.php b/src/model/Menu.php
index 3d07d3f..d277259 100644
--- a/src/model/Menu.php
+++ b/src/model/Menu.php
@@ -18,10 +18,6 @@ class Menu extends Page
->createQuery('SELECT n FROM App\Entity\Page n WHERE n.parent IS null') // :Doctrine\ORM\Query
->getResult(); // :array de Page
- if(count($bulk_data) === 0){
- fillStartingDatabase($entityManager); // => installation.php
- }
-
foreach($bulk_data as $first_level_entries){
// dans le menu
if($first_level_entries->isInMenu()){
diff --git a/src/model/entities/AppMetadata.php b/src/model/entities/AppMetadata.php
new file mode 100644
index 0000000..ae42ad9
--- /dev/null
+++ b/src/model/entities/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/entities/User.php b/src/model/entities/User.php
index 26802e2..36bc4db 100644
--- a/src/model/entities/User.php
+++ b/src/model/entities/User.php
@@ -22,15 +22,24 @@ class User
#[ORM\Column(type: "string", length: 255, unique: true)] // risque de modifier son mot de passe sans s'apercevoir qu'il fonctionne encore sur un autre compte
private string $login;
+ #[ORM\Column(type: "string", length: 50)]
+ private string $role;
+
#[ORM\Column(type: "string", length: 255)]
private string $password;
- public function __construct(string $login, string $password)
+ public function __construct(string $login, string $role, string $password)
{
$this->login = $login;
+ $this->role = $role;
$this->password = $password;
}
+ public function getId(): int
+ {
+ return $this->id_user;
+ }
+
public function getLogin(): string
{
return $this->login;
@@ -39,6 +48,14 @@ class User
{
$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;
diff --git a/src/router.php b/src/router.php
deleted file mode 100644
index ff219da..0000000
--- a/src/router.php
+++ /dev/null
@@ -1,384 +0,0 @@
- 1er test, méthode http: GET, POST ou autre chose
-=> 2ème test, type de contenu (méthode POST uniquement):
-"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 $_SESSION['admin'] est vrai
-*/
-
-declare(strict_types=1);
-
-if($request->getMethod() === 'GET'){
- // table "user" vide
- if(!UserController::existUsers($entityManager)){
- require AbstractBuilder::VIEWS_PATH . 'user_create.php';
- die;
- }
-
- // bouton déconnexion (méthode GET parce que l'utilisateur ne modifie plus de données à partir de là)
- if($request->query->has('action') && $request->query->get('action') === 'deconnection'){
- UserController::disconnect($entityManager);
- }
-
- // articles suivants
- if($request->query->has('fetch') && $request->query->get('fetch') === 'next_articles'){
- ArticleController::fetch($entityManager, $request);
- }
-
- // données du calendrier
- // création du calendrier et changement de dates affichées (boutons flèches mais pas changement de vue)
- if($request->query->has('action') && $request->query->get('action') === 'get_events'
- && $request->query->has('start') && $request->query->has('end') && empty($request->getPayload()->all())) // getPayload ne récupère pas que des POST
- {
- CalendarController::getData($entityManager);
- }
-
- // pages interdites
- if(!$_SESSION['admin'] && in_array(CURRENT_PAGE, ['menu_paths', 'new_page', 'user_edit', 'emails'])){
- header('Location: ' . new URL);
- die;
- }
-
- if($_SESSION['admin'] === true){
- // ...
- }
-
- // construction d'une page
- $response = (new ViewController)->buildView($entityManager, $request); // utilise Model
- // parenthèses nécéssaires autour de l'instanciation pour PHP < 8.4
-}
-
-
-elseif($request->getMethod() === 'POST'){
- /* -- contrôleurs appellables par tout le monde -- */
-
- // table "user" vide
- if(!UserController::existUsers($entityManager)){
- UserController::createUser($entityManager);
- }
-
- // requêtes JSON avec fetch()
- if($_SERVER['CONTENT_TYPE'] === 'application/json')
- {
- $json = json_decode($request->getContent(), true); // = json_decode(file_get_contents('php://input'), true);
-
- if(isset($_GET['action']))
- {
- // formulaire de contact
- if($_GET['action'] === 'send_email'){
- ContactFormController::sendVisitorEmail($entityManager, $json);
- }
- }
- }
-
- // envoi formulaire HTML
- elseif($_SERVER['CONTENT_TYPE'] === 'application/x-www-form-urlencoded'){
- // tentative de connexion
- if($request->query->has('action') && $request->query->get('action') === 'connection'){
- //$response =
- UserController::connect($entityManager);
- }
- }
-
-
- if($_SESSION['admin'] === true)
- {
- /* -- requêtes AJAX -- */
-
- // requêtes JSON avec fetch()
- if($_SERVER['CONTENT_TYPE'] === 'application/json')
- {
- $json = json_decode($request->getContent(), true); // = json_decode(file_get_contents('php://input'), true);
-
- if($request->query->has('action'))
- {
- /* -- manipulation des articles -- */
- if($_GET['action'] === 'editor_submit' && isset($json['id']) && isset($json['content'])){
- ArticleController::editorSubmit($entityManager, $json);
- }
- elseif($_GET['action'] === 'delete_article' && isset($json['id'])){
- $response = ArticleController::deleteArticle($entityManager, $json); // version AJAX
- }
- elseif($_GET['action'] === 'switch_positions' && isset($json['id1']) && isset($json['id2'])){
- ArticleController::switchPositions($entityManager, $json);
- }
- elseif($_GET['action'] === 'date_submit' && isset($json['id']) && isset($json['date'])){
- ArticleController::dateSubmit($entityManager, $json);
- }
-
- /* -- bloc Formulaire -- */
- elseif($_GET['action'] === 'keep_emails'){
- ContactFormController::keepEmails($entityManager, $json);
- }
- elseif($_GET['action'] === 'set_retention_period'){
- ContactFormController::setEmailsRetentionPeriod($entityManager, $json);
- }
- elseif($_GET['action'] === 'set_email_param'){
- ContactFormController::setEmailParam($entityManager, $json);
- }
- elseif($_GET['action'] === 'test_email'){
- ContactFormController::sendTestEmail($entityManager, $json);
- }
-
- /* -- page emails -- */
- elseif($_GET['action'] === 'delete_email'){
- ContactFormController::deleteEmail($entityManager, $json);
- }
- elseif($_GET['action'] === 'toggle_sensitive_email'){
- ContactFormController::toggleSensitiveEmail($entityManager, $json);
- }
-
- /* -- upload d'image dans tinymce par copier-coller -- */
- // collage de HTML contenant une ou plusieurs balises
- elseif($request->query->get('action') === 'upload_image_url'){
- ImageUploadController::uploadImageHtml();
- }
- // collage d'une image (code base64 dans le presse-papier) non encapsulée dans du HTML
- elseif($request->query->get('action') === 'upload_image_base64'){
- ImageUploadController::uploadImageBase64();
- }
-
-
- /* -- requêtes spécifiques au calendrier -- */
- elseif($request->query->get('action') === 'new_event'){
- CalendarController::newEvent($json, $entityManager);
- }
- elseif($request->query->get('action') === 'update_event'){
- CalendarController::updateEvent($json, $entityManager);
- }
- elseif($request->query->get('action') === 'remove_event'){
- CalendarController::removeEvent($json, $entityManager);
- }
- else{
- echo json_encode(['success' => false]);
- die;
- }
- }
-
- /* -- site entier (header, footer, favicon) -- */
- elseif($request->query->has('head_foot_text')){
- HeadFootController::setTextData($entityManager, $request->query->get('head_foot_text'), $json);
- }
- elseif($request->query->has('head_foot_social_check')){
- HeadFootController::displaySocialNetwork($entityManager, $request->query->get('head_foot_social_check'), $json);
- }
-
- /* -- page Menu et chemins -- */
- elseif(isset($_GET['menu_edit']))
- {
- // ne suit pas la règle, faire ça dans un contrôleur?
- Model::$menu = new Menu($entityManager); // récupération des données
-
- // flèche gauche <=: position = position du parent + 1, parent = grand-parent, recalculer les positions
- if($_GET['menu_edit'] === 'move_one_level_up' && isset($json['id'])){
- MenuAndPathsController::MoveOneLevelUp($entityManager, $json);
- }
- // flèche droite =>: position = nombre d'éléments de la fraterie + 1, l'élément précédent devient le parent
- elseif($_GET['menu_edit'] === 'move_one_level_down' && isset($json['id'])){
- MenuAndPathsController::MoveOneLevelDown($entityManager, $json);
- }
- elseif($_GET['menu_edit'] === 'switch_positions' && isset($json['id1']) && isset($json['id2'])){
- MenuAndPathsController::switchPositions($entityManager, $json);
- }
- elseif($_GET['menu_edit'] === 'display_in_menu' && isset($json['id']) && isset($json['checked'])){
- MenuAndPathsController::displayInMenu($entityManager, $json);
- }
- elseif($_GET['menu_edit'] === 'url_edit' && isset($json['id']) && isset($json['field']) && isset($json['input_data'])){
- MenuAndPathsController::editUrl($entityManager, $json);
- }
- }
-
- /* -- mode Modification d'une page -- */
- // partie "page"
- elseif(isset($_GET['page_edit']))
- {
- // titre de la page
- if($_GET['page_edit'] === 'page_title'){
- PageManagementController::setPageTitle($entityManager, $json);
- }
- // description dans les métadonnées
- elseif($_GET['page_edit'] === 'page_description'){
- PageManagementController::setPageDescription($entityManager, $json);
- }
- }
-
- // partie "blocs"
- elseif($request->query->has('bloc_edit'))
- {
- if($request->query->get('bloc_edit') === 'rename_page_bloc'){
- PageManagementController::renameBloc($entityManager, $json);
- }
- elseif($request->query->get('bloc_edit') === 'switch_blocs_positions'){
- PageManagementController::SwitchBlocsPositions($entityManager, $json);
- }
- elseif($request->query->get('bloc_edit') === 'change_articles_order'){
- PageManagementController::changeArticlesOrder($entityManager, $json);
- }
- elseif($request->query->get('bloc_edit') === 'change_presentation'){
- PageManagementController::changePresentation($entityManager, $json);
- }
- elseif($request->query->get('bloc_edit') === 'change_cols_min_width'){
- PageManagementController::changeColsMinWidth($entityManager, $json);
- }
- elseif($request->query->get('bloc_edit') === 'change_pagination_limit'){
- PageManagementController::changePaginationLimit($entityManager, $json);
- }
- }
- }
-
- // upload avec FormData
- elseif(strpos($_SERVER['CONTENT_TYPE'], 'multipart/form-data') !== false)
- {
- // dans tinymce avec le plugin (bouton "insérer une image" de l'éditeur ou glisser-déposer)
- if($request->query->has('action') && $request->query->get('action') === 'upload_image_tinymce'){
- ImageUploadController::imageUploadTinyMce();
- }
- // dans tinymce, des quatre méthodes: bouton "link", drag & drop, html, base64
- elseif($request->query->has('action') && $request->query->get('action') === 'upload_file_tinymce'){
- FileUploadController::fileUploadTinyMce();
- }
- elseif($request->query->has('head_foot_image')){
- HeadFootController::uploadAsset($entityManager, $request->query->get('head_foot_image'));
- }
- }
-
- // requêtes XMLHttpRequest
- elseif(isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest')
- {
- //echo "requête XMLHttpRequest reçue par le serveur";
- echo json_encode(['success' => false]); // noyer le poisson en laissant penser que le site gère les requêtes XHR
- die;
- }
-
- /* -- envoi formulaire HTML -- */
- elseif($_SERVER['CONTENT_TYPE'] === 'application/x-www-form-urlencoded')
- {
- if($request->query->has('action') && $request->query->get('action') === 'delete_article' && isset($_GET['id'])){
- $response = ArticleController::deleteArticle($entityManager, $_GET); // version formulaire
- }
-
- /* -- nouvelle page -- */
- elseif(isset($_POST['page_name']) && $_POST['page_name'] !== null
- && isset($_POST['page_name_path']) && $_POST['page_name_path'] !== null
- && isset($_POST['page_location']) && $_POST['page_location'] !== null
- && isset($_POST['page_description']) && $_POST['page_description'] !== null
- && isset($_POST['new_page_hidden']) && $_POST['new_page_hidden'] === '')
- {
- PageManagementController::newPage($entityManager, $_POST);
- }
-
- /* -- suppression d'une page -- */
- elseif(isset($_POST['page_id']) && $_POST['page_id'] !== null
- && isset($_POST['submit_hidden']) && $_POST['submit_hidden'] === '')
- {
- PageManagementController::deletePage($entityManager);
- }
-
-
- /* -- mode Modification d'une page -- */
-
- // modification du chemins en snake_case
- elseif(isset($_POST['page_menu_path']) && $_POST['page_menu_path'] !== null
- && isset($_POST['page_id']) && $_POST['page_id'] !== null
- && isset($_POST['page_name_path_hidden']) && $_POST['page_name_path_hidden'] === '')
- {
- PageManagementController::updatePageMenuPath($entityManager);
- }
- // ajout d'un bloc dans une page
- elseif(isset($_POST['bloc_title']) && $_POST['bloc_title'] !== null
- && isset($_POST['bloc_select']) && $_POST['bloc_select'] !== null
- && isset($_POST['bloc_title_hidden']) && $_POST['bloc_title_hidden'] === '') // contrôle anti-robot avec input hidden
- {
- PageManagementController::addBloc($entityManager);
- }
- // suppression d'un bloc de page
- elseif(isset($_POST['delete_bloc_id']) && $_POST['delete_bloc_id'] !== null
- && isset($_POST['delete_bloc_hidden']) && $_POST['delete_bloc_hidden'] === '') // contrôle anti-robot avec input hidden
- {
- PageManagementController::deleteBloc($entityManager);
- }
-
-
- /* -- page Menu et chemins -- */
-
- // création d'une entrée de menu avec une URL
- elseif(isset($_POST["label_input"]) && isset($_POST["url_input"]) && isset($_POST["location"])){
- MenuAndPathsController::newUrlMenuEntry($entityManager);
- }
- // suppression d'une entrée de menu avec une URL
- elseif(isset($_POST['delete']) && isset($_POST['x']) && isset($_POST['y'])){ // 2 params x et y sont là parce qu'on a cliqué sur une image
- MenuAndPathsController::deleteUrlMenuEntry($entityManager);
- }
-
-
- /* -- page Mon compte -- */
- elseif($request->query->has('action') && $request->query->get('action') === 'update_username')
- {
- UserController::updateUsername($entityManager);
- }
- elseif($request->query->has('action') && $request->query->get('action') === 'update_password')
- {
- UserController::updatePassword($entityManager);
- }
-
- // redirection page d'accueil
- else{
- header("Location: " . new URL(['error' => 'paramètres inconnus']));
- die;
- }
- }
- // POST admin ne matchant pas
- else{
- echo json_encode(['success' => false]);
- die;
- }
- }
- // POST non admin ne matchant pas
- else{
- echo json_encode(['success' => false]);
- die;
- }
-}
-
-// méthode inconnue
-else{
- header("Location: " . new URL(['error' => 'tu fais quoi là mec?']));
- die;
-}
-
-
-
-/* -- utilisation de la réponse -- */
-if(isset($response)){
- // cas gérés (d'autres sont à prévoir): mauvais id de la page article, accès page création d'article sans être admin
- if($request->isMethod('GET') && $response->getStatusCode() == 302){ // 302 redirection temporaire
- header('Location: ' . new URL(['page' => $_GET['from'] ?? '']));
- }
- // redirection après traitement de formulaires HTTP
- elseif($request->getMethod() === 'POST' && $_SERVER['CONTENT_TYPE'] === 'application/x-www-form-urlencoded'){
- $response_data = json_decode(($response)->getContent(), true);
- $url = new URL(['page' => $_GET['from'] ?? '']);
- $url->addParams(['success' => $response_data['success'], 'message' => $response_data['message']]);
- header('Location: ' . $url);
- }
- // affichage d'une page OU requête AJAX
- else{
- $response->send();
- }
-}
-// pas utilisation de RESPONSE (cas destiné à disparaître)
-else{
- if($request->getMethod() === 'POST' && $_SERVER['CONTENT_TYPE'] === 'application/x-www-form-urlencoded'){
- header("Location: " . new URL(['error' => 'erreur côté serveur']));
- }
- else{
- http_response_code(500);
- echo "erreur côté serveur";
- }
-}
-//die; // inutile
\ No newline at end of file
diff --git a/src/service/AppMode.php b/src/service/AppMode.php
new file mode 100644
index 0000000..60b58bd
--- /dev/null
+++ b/src/service/AppMode.php
@@ -0,0 +1,56 @@
+getRepository(AppMetadata::class)->find('mode');
+ if(!$metadata){
+ self::$mode = 'maintenance';
+ }
+ else{
+ self::$mode = $metadata->getValue();
+ }
+ }
+
+ public static function is(string $mode): bool
+ {
+ return self::$mode === $mode;
+ }
+
+ public static function get(): string
+ {
+ return self::$mode;
+ }
+
+ public static function set(EntityManager $entityManager, string $mode): void
+ {
+ self::$mode = $mode;
+
+ $metadata = $entityManager->find(AppMetadata::class, 'mode');
+ if($metadata){
+ $metadata->setValue($mode);
+ }
+ else{
+ $metadata = new AppMetadata('mode', $mode);
+ $entityManager->persist($metadata);
+ }
+ $entityManager->flush();
+
+ /*self::$data = [
+ 'mode' => $mode,
+ 'since' => (new DateTimeImmutable())->format('c'),
+ 'by' => $by,
+ ];*/
+ }
+}
\ No newline at end of file
diff --git a/src/service/Captcha.php b/src/service/Captcha.php
new file mode 100644
index 0000000..d57f912
--- /dev/null
+++ b/src/service/Captcha.php
@@ -0,0 +1,55 @@
+ 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
new file mode 100644
index 0000000..e59f728
--- /dev/null
+++ b/src/service/Config.php
@@ -0,0 +1,86 @@
+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..6f4e93d
--- /dev/null
+++ b/src/service/EmailService.php
@@ -0,0 +1,102 @@
+ 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; // une adresse bidon est donnée à setFrom()
+ $email_from_name = $form_data->getData()['email_from_name'] ?? Config::$email_from_name; // = site web
+ $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, Config::$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..4677bef
--- /dev/null
+++ b/src/service/FormValidation.php
@@ -0,0 +1,215 @@
+data = $data;
+ $this->validation_strategy = $validation_strategy;
+ }
+
+ public function validate(): bool
+ {
+ $this->errors = [];
+
+ // pattern stratégie en une seule classe
+ switch($this->validation_strategy){
+ // bloc formulaire de contact
+ case 'email_send':
+ $this->emailStrategy();
+ break;
+ case 'email_params': // paramètrage en mode admin
+ $this->emailParamsStrategy();
+ break;
+
+ // formulaires pages spéciales
+ case 'create_user':
+ $this->createUserStrategy();
+ break;
+ case 'connection':
+ $this->connectionStrategy();
+ break;
+ case 'username_update':
+ $this->usernameUpdateStrategy();
+ break;
+ case 'password_update':
+ $this->passwordUpdateStrategy();
+ break;
+
+ default:
+ http_response_code(500); // c'est un peu comme jeter une exception
+ echo json_encode(['success' => false, 'error' => 'server_error']);
+ die;
+ }
+
+ $this->validated = true;
+ return empty($this->errors);
+ }
+
+ public function getErrors(): array
+ {
+ return $this->errors;
+ }
+
+ public function getField(string $field): string
+ {
+ return $this->validated ? $this->data[$field] : '';
+ }
+
+ // méthodes de validation
+ private function captchaValidate(bool $clean_session = true): void
+ {
+ $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){
+ $error = 'error_non_valid_captcha';
+ }
+ elseif($captcha_solution == 0){ // ne peut pas arriver, si?
+ $error = 'captcha_server_error';
+ }
+ elseif($captcha_try !== $captcha_solution){
+ $this->errors[] = 'bad_solution_captcha';
+ }
+ }
+
+ // 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 emailStrategy(): void
+ {
+ $this->captchaValidate(false);
+
+ if(!isset($this->data['name']) || empty($this->data['name'])
+ || !isset($this->data['email']) || empty($this->data['email'])
+ || !isset($this->data['message']) || empty($this->data['message'])
+ || !isset($this->data['hidden']) || !empty($this->data['hidden'])){
+ $this->errors[] = 'missing_fields';
+ }
+
+ elseif(!filter_var(trim($this->data['email']), FILTER_VALIDATE_EMAIL)){
+ $this->errors[] = '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']);
+ }
+ private function emailParamsStrategy(): void
+ {
+ if(!isset($this->data['id'], $this->data['what_param'], $this->data['value'], $this->data['hidden'])
+ || !empty($this->data['hidden'])){
+ $this->errors[] = 'missing_fields';
+ }
+
+ elseif($this->data['value'] !== ''){
+ if(!in_array($this->data['what_param'], ['smtp_host', 'smtp_secure', 'smtp_username', 'smtp_password', 'email_dest'])){
+ $this->errors[] = 'unknown_parameter';
+ }
+ elseif($this->data['what_param'] === 'smtp_username' || $this->data['what_param'] === 'email_dest'){
+ if(!filter_var($this->data['value'], FILTER_VALIDATE_EMAIL)){
+ $this->errors[] = 'invalide_email_address';
+ }
+ }
+ }
+
+ // htmlspecialchars exécutés à l'affichage dans FormBuilder
+ }
+ private function createUserStrategy(): void
+ {
+ $this->captchaValidate();
+
+ // test mauvais paramètres
+ if(!isset($this->data['login']) || empty($this->data['login'])
+ || !isset($this->data['password']) || empty($this->data['password'])
+ || !isset($this->data['password_confirmation']) || empty($this->data['password_confirmation'])
+ || !isset($this->data['create_user_hidden']) || !empty($this->data['create_user_hidden']))
+ {
+ $this->errors[] = 'bad_login_or_password';
+ }
+
+ if($this->data['password'] !== $this->data['password_confirmation']){
+ $this->errors[] = 'different_passwords';
+ }
+
+ if($this->data['login'] !== self::removeSpacesTabsCRLF(htmlspecialchars($this->data['login']))
+ || $this->data['password'] !== self::removeSpacesTabsCRLF(htmlspecialchars($this->data['password']))){
+ $this->errors[] = 'forbidden_characters';
+ }
+ }
+ private function connectionStrategy(): void
+ {
+ $this->captchaValidate();
+
+ if(!isset($this->data['login']) || empty($this->data['login'])
+ || !isset($this->data['password']) || empty($this->data['password'])
+ || !isset($this->data['connection_hidden']) || !empty($this->data['connection_hidden']))
+ {
+ $this->errors[] = 'bad_login_or_password';
+ }
+ }
+ private function usernameUpdateStrategy(): void
+ {
+ $this->captchaValidate();
+
+ if(!isset($this->data['login']) || empty($this->data['login'])
+ || !isset($this->data['password']) || empty($this->data['password'])
+ || !isset($this->data['new_login']) || empty($this->data['new_login'])
+ || !isset($this->data['modify_username_hidden']) || !empty($this->data['modify_username_hidden']))
+ {
+ $this->errors[] = 'bad_login_or_password';
+ }
+
+ $new_login = self::removeSpacesTabsCRLF(htmlspecialchars($this->data['new_login']));
+ if($new_login !== $this->data['new_login']){
+ $this->errors[] = 'forbidden_characters';
+ }
+
+ if($this->data['login'] !== $_SESSION['user']['username']){
+ $this->errors[] = 'bad_login_or_password';
+ }
+ if($this->data['login'] === $new_login){
+ $this->errors[] = 'same_username_as_before';
+ }
+ }
+ private function passwordUpdateStrategy(): void
+ {
+ $this->captchaValidate();
+
+ if(!isset($this->data['login']) || empty($this->data['login'])
+ || !isset($this->data['password']) || empty($this->data['password'])
+ || !isset($this->data['new_password']) || empty($this->data['new_password'])
+ || !isset($this->data['modify_password_hidden']) || !empty($this->data['modify_password_hidden']))
+ {
+ $this->errors[] = 'bad_login_or_password';
+ }
+
+ $new_password = self::removeSpacesTabsCRLF(htmlspecialchars($this->data['new_password']));
+ if($new_password !== $this->data['new_password']){
+ $this->errors[] = 'forbidden_characters';
+ }
+
+ if($this->data['login'] !== $_SESSION['user']['username']){
+ $this->errors[] = 'bad_login_or_password';
+ }
+ if($this->data['password'] === $new_password){
+ $this->errors[] = 'same_password_as_before';
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/service/Installation.php b/src/service/Installation.php
new file mode 100644
index 0000000..5c2a901
--- /dev/null
+++ b/src/service/Installation.php
@@ -0,0 +1,209 @@
+l'extension " . $extension . ' 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;
+ }
+ 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 = 0700;
+ $droits_fichiers = 0600;
+
+ if(!file_exists('user_data')){
+ // créer le dossier user_data
+ mkdir('user_data/');
+ chmod('user_data/', $droits_dossiers);
+ 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
+ Aide: "serveur web" se nomme "www-data" sur debian et ubuntu, il s\'appelera "http" sur d\'autres distributions.
';
+ die;
+ }
+
+ 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
+ Quand vous aurez terminé votre config.ini, donnez-lui par sécurité des droits 600.
';
+ die;
+ }
+ else{
+ // droits du config.ini
+ /*if(substr(sprintf('%o', fileperms('../config/config.ini')), -4) != 600){
+ chmod('../config/config.ini', $droits_fichiers);
+ }*/
+
+ // tester les liens internes
+ //
+
+ // le test de connexion à la BDD est dans le doctrine bootstrap
+ }
+ }
+
+
+ /* création d'un site minimal avec une page d'accueil à la toute 1ère visite du site, ne doit surtout pas être exécutée une seconde fois */
+
+ // protection 1 utilisé à chaque requête
+ static private function isFirstRun(EntityManager $entityManager): bool
+ {
+ $metadata = $entityManager->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;
+ }
+
+ // 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();
+ }
+
+ static public function fillStartingDatabase(EntityManager $entityManager): void
+ {
+ if(!Installation::isFirstRun($entityManager)){
+ return;
+ }
+
+ // la BDD n'est pas vierge, on ne touche à rien
+ if(!self::areTablesEmpty($entityManager)){
+ return;
+ }
+
+ /* -- 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');
+
+ /* -- 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);
+
+ /* -- 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);
+
+ /* -- 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);
+
+ /* -- table node_data -- */
+ $entityManager->persist($head_data);
+ $entityManager->persist($header_data);
+ $entityManager->persist($footer_data);
+ $entityManager->persist($emails_data);
+
+ $entityManager->flush();
+
+ // empêcher la réutilisation de cette fonction
+ self::preventReinstallation($entityManager);
+
+ // fin de l'installation
+ AppMode::set($entityManager, 'run');
+
+ // recharger la page?
+ //header('Location: ' . new URL);
+ }
+}
\ No newline at end of file
diff --git a/src/service/Security.php b/src/service/Security.php
new file mode 100644
index 0000000..356f4f4
--- /dev/null
+++ b/src/service/Security.php
@@ -0,0 +1,110 @@
+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