aboutsummaryrefslogtreecommitdiff
path: root/src/service
diff options
context:
space:
mode:
Diffstat (limited to 'src/service')
-rw-r--r--src/service/AppMode.php56
-rw-r--r--src/service/Captcha.php55
-rw-r--r--src/service/Config.php86
-rw-r--r--src/service/EmailService.php102
-rw-r--r--src/service/FormValidation.php215
-rw-r--r--src/service/Installation.php209
-rw-r--r--src/service/Security.php110
-rw-r--r--src/service/URL.php88
-rw-r--r--src/service/router.php384
-rw-r--r--src/service/session.php86
10 files changed, 1391 insertions, 0 deletions
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 @@
1<?php
2// src/service/AppMode.php
3
4// comme dans AppMetadata, prévoir d'ajouter des champs "since" et "by" (qui a changé quoi quel jour?)
5
6declare(strict_types=1);
7
8use App\Entity\AppMetadata;
9use Doctrine\ORM\EntityManager;
10
11class AppMode
12{
13 private static string $mode;
14
15 public static function load(EntityManager $entityManager): void
16 {
17 $metadata = $entityManager->getRepository(AppMetadata::class)->find('mode');
18 if(!$metadata){
19 self::$mode = 'maintenance';
20 }
21 else{
22 self::$mode = $metadata->getValue();
23 }
24 }
25
26 public static function is(string $mode): bool
27 {
28 return self::$mode === $mode;
29 }
30
31 public static function get(): string
32 {
33 return self::$mode;
34 }
35
36 public static function set(EntityManager $entityManager, string $mode): void
37 {
38 self::$mode = $mode;
39
40 $metadata = $entityManager->find(AppMetadata::class, 'mode');
41 if($metadata){
42 $metadata->setValue($mode);
43 }
44 else{
45 $metadata = new AppMetadata('mode', $mode);
46 $entityManager->persist($metadata);
47 }
48 $entityManager->flush();
49
50 /*self::$data = [
51 'mode' => $mode,
52 'since' => (new DateTimeImmutable())->format('c'),
53 'by' => $by,
54 ];*/
55 }
56} \ 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 @@
1<?php
2// src/service/Captcha.php
3//
4// la solution est stockée dans une unique variable $_SESSION['captcha']
5// => on pourrait appliquer le pattern "singleton" (justification = le captcha devient une sorte de ressource partagée)
6
7declare(strict_types=1);
8
9class Captcha
10{
11 private int $a;
12 private int $b;
13
14 public function __construct(){
15 $this->a = rand(2, 9);
16 $this->b = rand(2, 9);
17 }
18
19 public function getA(): string
20 {
21 return $this->toLettersFrench($this->a);
22 }
23 public function getB(): string
24 {
25 return $this->toLettersFrench($this->b);
26 }
27 public function getSolution(): int
28 {
29 return ($this->a * $this->b);
30 }
31
32 private function toLettersFrench(int $number): string
33 {
34 return match($number){
35 2 => 'deux',
36 3 => 'trois',
37 4 => 'quatre',
38 5 => 'cinq',
39 6 => 'six',
40 7 => 'sept',
41 8 => 'huit',
42 9 => 'neuf',
43 default => '', // erreur
44 };
45 }
46
47 // (à déplacer dans FormValidation?)
48 static public function controlInput(string $input = '0'): int
49 {
50 // un POST est une chaîne qu'on doit convertir en nombre dans deux conditions:
51 // test de format: $input est un nombre
52 // test d'intégrité: supprimer les décimales avec (int) ne change pas la valeur du nombre
53 return is_numeric($input) && $input == (int)$input ? (int)$input : 0;
54 }
55} \ 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 @@
1<?php
2// src/service/Config.php
3
4declare(strict_types=1);
5
6class Config
7{
8 // BDD
9 static public string $db_host = 'localhost';
10 static public string $database = '';
11 static public string $db_driver = 'pdo_mysql';
12 static public string $user = '';
13 static public string $password = '';
14 static public string $table_prefix = '';
15
16 // classe URL
17 static public string $protocol = 'http';
18 static public string $index_path = '';
19 static public string $port = '80';
20
21 // e-mails
22 static public string $smtp_host = '';
23 static public string $smtp_username = '';
24 static public string $smtp_password = '';
25 static public string $smtp_secure = ''; // tls (smarttls) ou ssl (smtps) ou plain_text/chaine vide
26 static public string $email_from = 'mon_adresse@email.fr';
27 static public string $email_from_name = 'site web';
28 static public string $email_dest = '';
29 static public string $email_dest_name = 'destinataire formulaire';
30
31 // copier dans ce tableau les variables contenant des chemins
32 static private array $path_vars = [];
33
34 static public function load(string $file_path): void
35 {
36 if(file_exists($file_path))
37 {
38 // ce serait bien de gérer aussi les fichiers corrompus?
39 $raw_data = parse_ini_file($file_path);
40 self::hydrate($raw_data);
41 }
42 else
43 {
44 echo "<p>Le fichier config/config.ini n'existe pas ou n'est pas lisible.</p>";
45 }
46 define('TABLE_PREFIX', self::$table_prefix);
47 }
48
49 // renseigner les variables internes de Config
50 static private function hydrate(array $raw_data): void
51 {
52 foreach($raw_data as $field => $value)
53 {
54 if($value != '') // valeur par défaut
55 {
56 if(isset(self::$$field)) // le champ existe dans Config
57 {
58 // problème du slash à la fin du nom d'un dossier
59 $value = self::slashAtEndOfPath($field, $value);
60 self::$$field = $value;
61 }
62 else
63 {
64 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";
65 }
66 }
67 /*else
68 {
69 echo "debug: le champ " . $field . " est vide, la valeur par défaut " . self::$$field . " sera utilisée.\n";
70 }*/
71 }
72 }
73
74
75 // pour que les chemins finissent toujours par un /
76 static private function slashAtEndOfPath(string $field, string $value): string
77 {
78 foreach(self::$path_vars as $item)
79 {
80 if($field === $item){
81 return !str_ends_with($value, '/') ? $value . '/' : $value;
82 }
83 }
84 return $value;
85 }
86}
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 @@
1<?php
2// src/service/EmailService.php
3
4declare(strict_types=1);
5
6use PHPMailer\PHPMailer\PHPMailer;
7//use PHPMailer\PHPMailer\Exception;
8use Doctrine\ORM\EntityManager;
9use App\Entity\Email;
10use App\Entity\NodeData;
11
12class EmailService
13{
14 const KEEP_EMAILS_DEFAULT = false;
15
16 static public function send(EntityManager $entityManager, NodeData $form_data, bool $test_email, string $name = '', string $email = '', string $message = ''): bool
17 {
18 $mail = new PHPMailer(true); // true => exceptions
19 $mail->CharSet = 'UTF-8';
20
21 $smtp_host = $form_data->getData()['smtp_host'] ?? Config::$smtp_host;
22 $smtp_secure = $form_data->getData()['smtp_secure'] ?? Config::$smtp_secure;
23 $smtp_username = $form_data->getData()['smtp_username'] ?? Config::$smtp_username;
24 $smtp_password = $form_data->getData()['smtp_password'] ?? Config::$smtp_password;
25 $email_from = $form_data->getData()['email_from'] ?? Config::$email_from; // une adresse bidon est donnée à setFrom()
26 $email_from_name = $form_data->getData()['email_from_name'] ?? Config::$email_from_name; // = site web
27 $email_dest = $form_data->getData()['email_dest'] ?? Config::$email_dest;
28 $email_dest_name = $form_data->getData()['email_dest_name'] ?? Config::$email_dest_name; // = destinataire formulaire
29
30 try{
31 // Paramètres du serveur
32 $mail->isSMTP();
33 $mail->Host = $smtp_host;
34 $mail->SMTPAuth = true;
35 $mail->Port = 25;
36
37 if($mail->SMTPAuth){
38 $mail->Username = $smtp_username; // e-mail
39 $mail->Password = $smtp_password;
40 $mail->SMTPSecure = $smtp_secure; // tls (starttls) ou ssl (smtps)
41 if($mail->SMTPSecure === 'tls'){
42 $mail->Port = 587;
43 }
44 elseif($mail->SMTPSecure === 'ssl'){
45 $mail->Port = 465;
46 }
47 }
48 //var_dump($mail->smtpConnect());die; // test de connexion
49
50 // Expéditeur et destinataire
51 // $email_from, $email_from_name et $email_dest_name sont modifiables uniquement dans le config.ini pour l'instant
52 $mail->setFrom(strtolower($email_from), $email_from_name);
53 $mail->addAddress(strtolower($email_dest), $email_dest_name);
54
55 // Contenu
56 $mail->isHTML(true);
57 if($test_email){
58 $mail->Subject = "TEST d'un envoi d'e-mail depuis le site web";
59 }
60 else{
61 $mail->Subject = 'Message envoyé par: ' . $name . ' (' . $email . ') depuis le site web';
62 }
63 $mail->Body = $message;
64 $mail->AltBody = $message;
65
66 $mail->send();
67
68 // copie en BDD
69 if(!$test_email && ($form_data->getData()['keep_emails'] ?? self::KEEP_EMAILS_DEFAULT)){
70 $db_email = new Email($name, $email, Config::$email_dest, $message, $form_data);
71 $entityManager->persist($db_email);
72 self::updateLastContactDate($entityManager, $email);
73 $entityManager->flush();
74 }
75
76 return true;
77 }
78 catch(Exception $e){
79 echo "Le message n'a pas pu être envoyé. Erreur : {$e} <br> {$mail->ErrorInfo}";
80 return false;
81 }
82 }
83
84 static public function updateLastContactDate(EntityManager $entityManager, string $sender): void
85 {
86 foreach($entityManager->getRepository('App\Entity\Email')->findAll() as $email){
87 $email->getSenderAddress() === $sender ? $email->updateLastContactDate() : null;
88 }
89 }
90
91 // peut être appelée par bin/clean_emails_cron.php
92 static public function cleanEmails(EntityManager $entityManager): void
93 {
94 $emails = $entityManager->getRepository('App\Entity\Email')->findAll();
95 foreach($emails as $email){
96 if($email->getDeletionDate() < new \DateTime()){
97 $entityManager->remove($email);
98 }
99 }
100 $entityManager->flush();
101 }
102} \ 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 @@
1<?php
2// src/service/FormValidation.php
3
4declare(strict_types=1);
5
6class FormValidation
7{
8 private array $data; // tableau associatif (probablement $_POST)
9 private string $validation_strategy; // à remplacer plus tard par un objet (pattern stratégie) d'interface ValidationStrategy
10 private array $errors;
11 private bool $validated = false;
12
13 public function __construct(array $data, string $validation_strategy){
14 $this->data = $data;
15 $this->validation_strategy = $validation_strategy;
16 }
17
18 public function validate(): bool
19 {
20 $this->errors = [];
21
22 // pattern stratégie en une seule classe
23 switch($this->validation_strategy){
24 // bloc formulaire de contact
25 case 'email_send':
26 $this->emailStrategy();
27 break;
28 case 'email_params': // paramètrage en mode admin
29 $this->emailParamsStrategy();
30 break;
31
32 // formulaires pages spéciales
33 case 'create_user':
34 $this->createUserStrategy();
35 break;
36 case 'connection':
37 $this->connectionStrategy();
38 break;
39 case 'username_update':
40 $this->usernameUpdateStrategy();
41 break;
42 case 'password_update':
43 $this->passwordUpdateStrategy();
44 break;
45
46 default:
47 http_response_code(500); // c'est un peu comme jeter une exception
48 echo json_encode(['success' => false, 'error' => 'server_error']);
49 die;
50 }
51
52 $this->validated = true;
53 return empty($this->errors);
54 }
55
56 public function getErrors(): array
57 {
58 return $this->errors;
59 }
60
61 public function getField(string $field): string
62 {
63 return $this->validated ? $this->data[$field] : '';
64 }
65
66 // méthodes de validation
67 private function captchaValidate(bool $clean_session = true): void
68 {
69 $captcha_solution = (isset($_SESSION['captcha']) && is_int($_SESSION['captcha'])) ? $_SESSION['captcha'] : 0;
70 $captcha_try = isset($this->data['captcha']) ? Captcha::controlInput($this->data['captcha']) : 0;
71 if($clean_session){
72 unset($_SESSION['captcha']);
73 }
74
75 if($captcha_try == 0){
76 $error = 'error_non_valid_captcha';
77 }
78 elseif($captcha_solution == 0){ // ne peut pas arriver, si?
79 $error = 'captcha_server_error';
80 }
81 elseif($captcha_try !== $captcha_solution){
82 $this->errors[] = 'bad_solution_captcha';
83 }
84 }
85
86 // erreurs à la création des mots de passe
87 static private function removeSpacesTabsCRLF(string $chaine): string
88 {
89 $cibles = [' ', "\t", "\n", "\r"]; // doubles quotes !!
90 return(str_replace($cibles, '', $chaine));
91 }
92
93
94 // stratégies
95 private function emailStrategy(): void
96 {
97 $this->captchaValidate(false);
98
99 if(!isset($this->data['name']) || empty($this->data['name'])
100 || !isset($this->data['email']) || empty($this->data['email'])
101 || !isset($this->data['message']) || empty($this->data['message'])
102 || !isset($this->data['hidden']) || !empty($this->data['hidden'])){
103 $this->errors[] = 'missing_fields';
104 }
105
106 elseif(!filter_var(trim($this->data['email']), FILTER_VALIDATE_EMAIL)){
107 $this->errors[] = 'bad_email_address';
108 }
109
110 $this->data['name'] = htmlspecialchars(trim($this->data['name']));
111 $this->data['email'] = htmlspecialchars(trim($this->data['email']));
112 $this->data['message'] = htmlspecialchars($this->data['message']);
113 }
114 private function emailParamsStrategy(): void
115 {
116 if(!isset($this->data['id'], $this->data['what_param'], $this->data['value'], $this->data['hidden'])
117 || !empty($this->data['hidden'])){
118 $this->errors[] = 'missing_fields';
119 }
120
121 elseif($this->data['value'] !== ''){
122 if(!in_array($this->data['what_param'], ['smtp_host', 'smtp_secure', 'smtp_username', 'smtp_password', 'email_dest'])){
123 $this->errors[] = 'unknown_parameter';
124 }
125 elseif($this->data['what_param'] === 'smtp_username' || $this->data['what_param'] === 'email_dest'){
126 if(!filter_var($this->data['value'], FILTER_VALIDATE_EMAIL)){
127 $this->errors[] = 'invalide_email_address';
128 }
129 }
130 }
131
132 // htmlspecialchars exécutés à l'affichage dans FormBuilder
133 }
134 private function createUserStrategy(): void
135 {
136 $this->captchaValidate();
137
138 // test mauvais paramètres
139 if(!isset($this->data['login']) || empty($this->data['login'])
140 || !isset($this->data['password']) || empty($this->data['password'])
141 || !isset($this->data['password_confirmation']) || empty($this->data['password_confirmation'])
142 || !isset($this->data['create_user_hidden']) || !empty($this->data['create_user_hidden']))
143 {
144 $this->errors[] = 'bad_login_or_password';
145 }
146
147 if($this->data['password'] !== $this->data['password_confirmation']){
148 $this->errors[] = 'different_passwords';
149 }
150
151 if($this->data['login'] !== self::removeSpacesTabsCRLF(htmlspecialchars($this->data['login']))
152 || $this->data['password'] !== self::removeSpacesTabsCRLF(htmlspecialchars($this->data['password']))){
153 $this->errors[] = 'forbidden_characters';
154 }
155 }
156 private function connectionStrategy(): void
157 {
158 $this->captchaValidate();
159
160 if(!isset($this->data['login']) || empty($this->data['login'])
161 || !isset($this->data['password']) || empty($this->data['password'])
162 || !isset($this->data['connection_hidden']) || !empty($this->data['connection_hidden']))
163 {
164 $this->errors[] = 'bad_login_or_password';
165 }
166 }
167 private function usernameUpdateStrategy(): void
168 {
169 $this->captchaValidate();
170
171 if(!isset($this->data['login']) || empty($this->data['login'])
172 || !isset($this->data['password']) || empty($this->data['password'])
173 || !isset($this->data['new_login']) || empty($this->data['new_login'])
174 || !isset($this->data['modify_username_hidden']) || !empty($this->data['modify_username_hidden']))
175 {
176 $this->errors[] = 'bad_login_or_password';
177 }
178
179 $new_login = self::removeSpacesTabsCRLF(htmlspecialchars($this->data['new_login']));
180 if($new_login !== $this->data['new_login']){
181 $this->errors[] = 'forbidden_characters';
182 }
183
184 if($this->data['login'] !== $_SESSION['user']['username']){
185 $this->errors[] = 'bad_login_or_password';
186 }
187 if($this->data['login'] === $new_login){
188 $this->errors[] = 'same_username_as_before';
189 }
190 }
191 private function passwordUpdateStrategy(): void
192 {
193 $this->captchaValidate();
194
195 if(!isset($this->data['login']) || empty($this->data['login'])
196 || !isset($this->data['password']) || empty($this->data['password'])
197 || !isset($this->data['new_password']) || empty($this->data['new_password'])
198 || !isset($this->data['modify_password_hidden']) || !empty($this->data['modify_password_hidden']))
199 {
200 $this->errors[] = 'bad_login_or_password';
201 }
202
203 $new_password = self::removeSpacesTabsCRLF(htmlspecialchars($this->data['new_password']));
204 if($new_password !== $this->data['new_password']){
205 $this->errors[] = 'forbidden_characters';
206 }
207
208 if($this->data['login'] !== $_SESSION['user']['username']){
209 $this->errors[] = 'bad_login_or_password';
210 }
211 if($this->data['password'] === $new_password){
212 $this->errors[] = 'same_password_as_before';
213 }
214 }
215} \ 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 @@
1<?php
2// src/service/Installation.php
3
4declare(strict_types=1);
5
6use App\Entity\AppMetadata;
7use App\Entity\Page;
8use App\Entity\Node;
9use App\Entity\NodeData;
10use Doctrine\ORM\EntityManager;
11
12class Installation
13{
14 static public function phpDependancies(): void
15 {
16 $flag = false;
17 //$extensions = ['pdo_mysql', 'mbstring', 'ctype', 'json', 'tokenizer', 'zip', 'dom']; // les 5 premières sont pour doctrine
18 $extensions = ['pdo_mysql', 'mbstring', 'ctype', 'json', 'tokenizer'];
19 foreach($extensions as $extension){
20 if(!extension_loaded($extension))
21 {
22 echo("<p>l'extension <b>" . $extension . '</b> est manquante</p>');
23 $flag = true;
24 }
25 }
26 if(!extension_loaded('imagick') && !extension_loaded('gd')){
27 echo("<p>il manque une de ces extensions au choix pour le traitement des images: <b>imagick</b> (de préférence) ou <b>gd</b>.</p>");
28 $flag = true;
29 }
30 if($flag){
31 echo '<p>Réalisez les actions nécéssaires sur le serveur ou contactez l\'administrateur du site.<br>
32 Quand le problème sera résolu, il vous suffira de <a href="#">recharger la page<a>.</p>';
33 die;
34 }
35 }
36
37 static public function checkFilesAndFoldersRights(): void
38 {
39 // -- droits des fichiers et dossiers --
40 $droits_dossiers = 0700;
41 $droits_fichiers = 0600;
42
43 if(!file_exists('user_data')){
44 // créer le dossier user_data
45 mkdir('user_data/');
46 chmod('user_data/', $droits_dossiers);
47 echo '<p style="color: red;">Le dossier public/user_data introuvable et le serveur n\'a pas la permission de le créer.<br>
48 Pour faire ça bien:<br>sudo -u "serveur web" mkdir /chemin/du/site/public/user_data</p>
49 <p>Aide: "serveur web" se nomme "www-data" sur debian et ubuntu, il s\'appelera "http" sur d\'autres distributions.</p>';
50 die;
51 }
52
53 if(!file_exists('../config/config.ini')){
54 // aide à la création du config.ini
55 echo '<p>Le fichier config/config.ini est introuvable.</p>';
56 echo '<p>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.<br>
57 Un modèle est disponible, il s\'agit du fichier config/config-template.ini</p>
58 <p>Quand vous aurez terminé votre config.ini, donnez-lui par sécurité des droits 600.</p>';
59 die;
60 }
61 else{
62 // droits du config.ini
63 /*if(substr(sprintf('%o', fileperms('../config/config.ini')), -4) != 600){
64 chmod('../config/config.ini', $droits_fichiers);
65 }*/
66
67 // tester les liens internes
68 //
69
70 // le test de connexion à la BDD est dans le doctrine bootstrap
71 }
72 }
73
74
75 /* 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 */
76
77 // protection 1 utilisé à chaque requête
78 static private function isFirstRun(EntityManager $entityManager): bool
79 {
80 $metadata = $entityManager->getRepository(AppMetadata::class)->find('installed');
81 return !$metadata || $metadata->getValue() !== '1';
82 }
83
84 // protection 2, qui vérifie vraiment que les tables concernées sont vides
85 static private function areTablesEmpty(EntityManager $entityManager): bool
86 {
87 $empty = true;
88 $entities = ['Page', 'Node', 'NodeData'];
89 foreach($entities as $entity){
90 $entity = 'App\Entity\\' . $entity; // nécéssaire quand on insère le nom avec une variable
91
92 if($entityManager
93 ->createQuery("SELECT e FROM $entity e")
94 ->setMaxResults(1)
95 ->getOneOrNullResult()){
96 $empty = false;
97 }
98 }
99
100 // cas anormal détecté, on remet en place la clé "installed"
101 if(!$empty){
102 self::preventReinstallation($entityManager);
103 }
104
105 return $empty;
106 }
107
108 // met en place la protection
109 static private function preventReinstallation(EntityManager $entityManager): void
110 {
111 $metadata = $entityManager->getRepository(AppMetadata::class)->find('installed');
112 if($metadata){
113 $metadata->setValue('1');
114 }
115 else{
116 $metadata = new AppMetadata('installed', '1');
117 $entityManager->persist($metadata);
118 }
119 $entityManager->flush();
120 }
121
122 static public function fillStartingDatabase(EntityManager $entityManager): void
123 {
124 if(!Installation::isFirstRun($entityManager)){
125 return;
126 }
127
128 // la BDD n'est pas vierge, on ne touche à rien
129 if(!self::areTablesEmpty($entityManager)){
130 return;
131 }
132
133 /* -- table page -- */
134 // paramètres: name_page, end_of_path, reachable, in_menu, hidden, position, parent
135 $accueil = new Page('Accueil', 'accueil', "Page d'accueil", true, true, false, 1, NULL);
136 $article = new Page('Article', 'article', "", true, false, false, NULL, NULL);
137 $connection = new Page('Connexion', 'connection', "Connexion", true, false, false, NULL, NULL);
138 $my_account = new Page('Mon compte', 'user_edit', "Mon compte", true, false, false, NULL, NULL);
139 $menu_paths = new Page("Menu et chemins", 'menu_paths', "Menu et chemins", true, false, false, NULL, NULL);
140 $menu_paths->addCSS('menu');
141 $menu_paths->addJS('menu');
142 $new_page = new Page('Nouvelle page', 'new_page', "Nouvelle page", true, false, false, NULL, NULL);
143 $new_page->addCSS('new_page');
144 $new_page->addJS('new_page');
145 $emails = new Page("Courriels", 'emails', "Consulter les courriels en base de données", true, false, false, NULL, NULL);
146 $emails->addCSS('show_emails');
147 $emails->addJS('form');
148
149 /* -- table node -- */
150 // paramètres: name_node, article_timestamp, attributes, position, parent, page, article
151 $head = new Node('head', 1, NULL, NULL, NULL);
152 $header = new Node('header', 2, NULL, NULL, NULL);
153 $nav = new Node('nav', 1, $header, NULL, NULL);
154 $main = new Node('main', 3, NULL, NULL, NULL);
155 $footer = new Node('footer', 4, NULL, NULL, NULL);
156 $breadcrumb = new Node('breadcrumb', 2, $header, NULL, NULL);
157 $login = new Node('login', 1, $main, $connection, NULL);
158 $user_edit = new Node('user_edit', 1, $main, $my_account, NULL);
159 $bloc_edit_menu = new Node('menu', 1, $main, $menu_paths, NULL);
160 $bloc_new_page = new Node('new_page', 1, $main, $new_page, NULL);
161 $bloc_emails = new Node('show_emails', 1, $main, $emails, NULL);
162
163 /* -- table node_data -- */
164 // paramètres: data, node, images
165 $head_data = new NodeData([], $head);
166 $header_data = new NodeData([], $header);
167 $footer_data = new NodeData([], $footer);
168 $emails_data = new NodeData([], $bloc_emails);
169
170 /* -- table page -- */
171 $entityManager->persist($accueil);
172 $entityManager->persist($article);
173 $entityManager->persist($connection);
174 $entityManager->persist($my_account);
175 $entityManager->persist($menu_paths);
176 $entityManager->persist($new_page);
177 $entityManager->persist($emails);
178
179 /* -- table node -- */
180 $entityManager->persist($head);
181 $entityManager->persist($header);
182 $entityManager->persist($nav);
183 $entityManager->persist($main);
184 $entityManager->persist($footer);
185 $entityManager->persist($breadcrumb);
186 $entityManager->persist($login);
187 $entityManager->persist($user_edit);
188 $entityManager->persist($bloc_edit_menu);
189 $entityManager->persist($bloc_new_page);
190 $entityManager->persist($bloc_emails);
191
192 /* -- table node_data -- */
193 $entityManager->persist($head_data);
194 $entityManager->persist($header_data);
195 $entityManager->persist($footer_data);
196 $entityManager->persist($emails_data);
197
198 $entityManager->flush();
199
200 // empêcher la réutilisation de cette fonction
201 self::preventReinstallation($entityManager);
202
203 // fin de l'installation
204 AppMode::set($entityManager, 'run');
205
206 // recharger la page?
207 //header('Location: ' . new URL);
208 }
209} \ 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<?php
2// src/service/Security.php
3//
4// htmlawed nettoie les entrées de l'utilisateur, en particulier le html de l'éditeur
5
6declare(strict_types=1);
7
8class Security
9{
10 private static $configHtmLawed = array(
11 'safe'=>1, // protection contre les élements et attributs dangereux
12
13 // liste blanche d'éléments HTML
14 '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',
15
16 // liste noire d'attributs HTML
17 'deny_attribute'=> 'id, class' // on garde 'style'
18 );
19 // faire qu'un certain élément puisse n'avoir que certains attributs, regarder la doc
20 private static $specHtmLawed = '';
21
22 // obtenir du HTML non dangereur sans appliquer htmlspecialchars
23 public static function secureHTML(string $chaine): string
24 {
25 return trim(htmLawed($chaine, self::$configHtmLawed, self::$specHtmLawed));
26 }
27
28 public static function secureFileName(string $chaine): string
29 {
30 // sécuriser un nom avec chemin avec basename?
31 //$chaine = basename($chaine);
32
33 /*
34 - caractères interdits sous windows / \ : * ? " < > |
35 - mac autorise les /
36 - mac interdit :
37 - linux autorise tout sauf les /
38 - imagemagick ne supporte pas les :
39
40 - 'espace' fonctionne
41 - / remplacé par firefox en :
42 - \ retire ce qui est devant le \
43 - * fonctionne
44 - ? permet le téléchargement mais pas l'affichage
45 - " ne fonctionne pas, remplacé par %22, filtrer %22
46 - < > fonctionnent
47 - | fonctionne
48 - = fonctionne, mais je filtre parce qu'on en trouve dans une URL
49 - ' ` fonctionnent
50 - % fonctionne
51 - (){}[] fonctionnent
52 - ^ fonctionne
53 - # ne fonctionne pas
54 - ~ fonctionne
55 - & fonctionne
56 - ^ pas encore testé
57 */
58
59 // => on remplace tout par des _
60 // filtrer / et \ semble inutile
61
62 /*$cibles = [' ', '/', '\\', ':', '*', '?', '<', '>', '|', '=', "'", '`', '"', '%22', '#'];
63 $chaine = str_replace($cibles, '_', $chaine); // nécéssite l'extension mbstring
64 $chaine = mb_strtolower($chaine);
65 return($chaine);*/
66
67 $chaine = preg_replace('/[^a-zA-Z0-9_-]/', '_', $chaine); // ne garder que les lettres, chiffres, tirets et underscores
68 $chaine = preg_replace('/_+/', '_', $chaine); // doublons d'underscores
69 return trim($chaine, '_');
70
71 // les problèmes avec \ persistent !!
72 // => javascript
73 // malheureusement document.getElementById('upload').files[0].name = chaine; ne marche pas! interdit!
74 // javascript ne doit pas pouvoir accéder au système de fichiers
75 // solutions:
76 // - au lieu de fournir une chaine (le chemin du fichier), donner un objet à files[0].name
77 // - créer une copie du fichier et l'envoyer à la place
78 // - envoyer le fichier en AJAX
79 // - envoyer le nom du fichier à part puis renommer en PHP
80 }
81}
82
83// lien sans http://
84function fixLinks($data)
85{
86 // 1/
87 // 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://
88 // tomber ainsi sur une page d'erreur est parfaitement déroutant
89
90 // regex pour détecter les balises <a> et ajouter http:// au début des liens si nécessaire
91 $pattern = '#(<a[^>]+href=")((?!https?://)[^>]+>)#';
92 //$data = preg_replace($pattern, '$1http://$2', $data);
93
94 // 2/
95 // cas où la regex fait mal son boulot:
96 // l'erreur 404 est gérée par le .htaccess
97 // et le visiteur est redirigé à la page "menu"
98 // (ça ne règle pas le problème mais c'est mieux)
99
100 // 3/
101 // quand l'éditeur est ouvert (avant de valider l'article),
102 // le lien qu'on vient de créer apparaît dans l'infobulle,
103 // cliquer dessus ouvre un onglet sur une erreur 404
104 // solution partielle avec le .htaccess
105 //
106 // solution? fermer ce nouvel onglet avec echo '<SCRIPT>javascript:window.close()</SCRIPT>';
107 // comment déclencher le JS? en faisant qu'une erreur 404 causée pour cette raison soit particulière?
108
109 return($data);
110}
diff --git a/src/service/URL.php b/src/service/URL.php
new file mode 100644
index 0000000..5bd2594
--- /dev/null
+++ b/src/service/URL.php
@@ -0,0 +1,88 @@
1<?php
2// src/service/URL.php
3
4declare(strict_types=1);
5
6class URL implements Stringable
7{
8 static private string $protocol = 'http://';
9 static private string $host = '';
10 static private string $port;
11 static private string $path = '/index.php';
12 private array $params;
13 private string $anchor = '';
14
15 public function __construct(array $gets = [], string $anchor = ''){
16 $this->params = $gets;
17 if($anchor != ''){
18 $this->setAnchor($anchor);
19 }
20 }
21
22 // setters statiques
23 static public function setProtocol(string $protocol = 'http'): void
24 {
25 self::$protocol = $protocol === 'https' ? 'https://' : 'http://';
26 }
27 static public function setPort(int|string $port = 80): void
28 {
29 if((int)$port === 443){
30 self::$protocol = 'https://';
31 self::$port = '';
32 }
33 elseif((int)$port === 80){
34 self::$protocol = 'http://';
35 self::$port = '';
36 }
37 else{
38 self::$port = ':' . (string)$port;
39 }
40 }
41 static public function setHost(string $host): void
42 {
43 self::$host = $host;
44 }
45 static public function setPath(string $path): void
46 {
47 self::$path = '/' . ltrim($path, '/');
48 }
49
50 //setters normaux
51 public function addParams(array $gets): void
52 {
53 // array_merge est préféré à l'opérateur d'union +, si une clé existe déjà la valeur est écrasée
54 $this->params = array_merge($this->params, $gets);
55 }
56 public function setAnchor(string $anchor = ''): void
57 {
58 if($anchor != ''){
59 $this->anchor = '#' . ltrim($anchor, '#');
60 }
61 else{
62 $this->anchor = '';
63 }
64 }
65
66 private function makeParams(): string
67 {
68 $output = '';
69 $first = true;
70
71 foreach($this->params as $key => $value) {
72 if($first){
73 $output .= '?';
74 $first = false;
75 }
76 else{
77 $output .= '&';
78 }
79 $output .= $key . '=' . $value;
80 }
81 return $output;
82 }
83
84 public function __toString(): string
85 {
86 return self::$protocol . self::$host . self::$port . self::$path . $this->makeParams() . $this->anchor;
87 }
88} \ No newline at end of file
diff --git a/src/service/router.php b/src/service/router.php
new file mode 100644
index 0000000..fc6b028
--- /dev/null
+++ b/src/service/router.php
@@ -0,0 +1,384 @@
1<?php
2// src/service/router.php
3//
4/* fonctionnement:
5=> 1er test, méthode http: GET, POST ou autre chose
6=> 2ème test, type de contenu (méthode POST uniquement):
7"application/x-www-form-urlencoded" = formulaire
8"application/json" = requête AJAX avec fetch()
9"multipart/form-data" = upload d'image par tinymce
10$_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest' requête AJAX xhs, non utilisée
11=> 3ème test, comme le 2ème test mais uniquement si IS_ADMIN est vrai
12*/
13
14declare(strict_types=1);
15
16if($request->getMethod() === 'GET'){
17 // table "user" vide
18 if(!UserController::existUsers($entityManager)){
19 require AbstractBuilder::VIEWS_PATH . 'user_create.php';
20 die;
21 }
22
23 // bouton déconnexion (méthode GET parce que l'utilisateur ne modifie plus de données à partir de là)
24 if($request->query->has('action') && $request->query->get('action') === 'deconnection'){
25 UserController::disconnect($entityManager);
26 }
27
28 // articles suivants
29 if($request->query->has('fetch') && $request->query->get('fetch') === 'next_articles'){
30 ArticleController::fetch($entityManager, $request);
31 }
32
33 // données du calendrier
34 // création du calendrier et changement de dates affichées (boutons flèches mais pas changement de vue)
35 if($request->query->has('action') && $request->query->get('action') === 'get_events'
36 && $request->query->has('start') && $request->query->has('end') && empty($request->getPayload()->all())) // getPayload ne récupère pas que des POST
37 {
38 CalendarController::getData($entityManager);
39 }
40
41 // pages interdites
42 if(!IS_ADMIN && in_array(CURRENT_PAGE, ['menu_paths', 'new_page', 'user_edit', 'emails'])){
43 header('Location: ' . new URL);
44 die;
45 }
46
47 if(IS_ADMIN === true){
48 // ...
49 }
50
51 // construction d'une page
52 $response = (new ViewController)->buildView($entityManager, $request); // utilise Model
53 // parenthèses nécéssaires autour de l'instanciation pour PHP < 8.4
54}
55
56
57elseif($request->getMethod() === 'POST'){
58 /* -- contrôleurs appellables par tout le monde -- */
59
60 // table "user" vide
61 if(!UserController::existUsers($entityManager)){
62 UserController::createAdminUser($entityManager);
63 }
64
65 // requêtes JSON avec fetch()
66 if($_SERVER['CONTENT_TYPE'] === 'application/json')
67 {
68 $json = json_decode($request->getContent(), true); // = json_decode(file_get_contents('php://input'), true);
69
70 if(isset($_GET['action']))
71 {
72 // formulaire de contact
73 if($_GET['action'] === 'send_email'){
74 ContactFormController::sendVisitorEmail($entityManager, $json);
75 }
76 }
77 }
78
79 // envoi formulaire HTML
80 elseif($_SERVER['CONTENT_TYPE'] === 'application/x-www-form-urlencoded'){
81 // tentative de connexion
82 if($request->query->has('action') && $request->query->get('action') === 'connection'){
83 //$response =
84 UserController::connect($entityManager);
85 }
86 }
87
88
89 if(IS_ADMIN === true)
90 {
91 /* -- requêtes AJAX -- */
92
93 // requêtes JSON avec fetch()
94 if($_SERVER['CONTENT_TYPE'] === 'application/json')
95 {
96 $json = json_decode($request->getContent(), true); // = json_decode(file_get_contents('php://input'), true);
97
98 if($request->query->has('action'))
99 {
100 /* -- manipulation des articles -- */
101 if($_GET['action'] === 'editor_submit' && isset($json['id']) && isset($json['content'])){
102 ArticleController::editorSubmit($entityManager, $json);
103 }
104 elseif($_GET['action'] === 'delete_article' && isset($json['id'])){
105 $response = ArticleController::deleteArticle($entityManager, $json); // version AJAX
106 }
107 elseif($_GET['action'] === 'switch_positions' && isset($json['id1']) && isset($json['id2'])){
108 ArticleController::switchPositions($entityManager, $json);
109 }
110 elseif($_GET['action'] === 'date_submit' && isset($json['id']) && isset($json['date'])){
111 ArticleController::dateSubmit($entityManager, $json);
112 }
113
114 /* -- bloc Formulaire -- */
115 elseif($_GET['action'] === 'keep_emails'){
116 ContactFormController::keepEmails($entityManager, $json);
117 }
118 elseif($_GET['action'] === 'set_retention_period'){
119 ContactFormController::setEmailsRetentionPeriod($entityManager, $json);
120 }
121 elseif($_GET['action'] === 'set_email_param'){
122 ContactFormController::setEmailParam($entityManager, $json);
123 }
124 elseif($_GET['action'] === 'test_email'){
125 ContactFormController::sendTestEmail($entityManager, $json);
126 }
127
128 /* -- page emails -- */
129 elseif($_GET['action'] === 'delete_email'){
130 ContactFormController::deleteEmail($entityManager, $json);
131 }
132 elseif($_GET['action'] === 'toggle_sensitive_email'){
133 ContactFormController::toggleSensitiveEmail($entityManager, $json);
134 }
135
136 /* -- upload d'image dans tinymce par copier-coller -- */
137 // collage de HTML contenant une ou plusieurs balises <img>
138 elseif($request->query->get('action') === 'upload_image_url'){
139 ImageUploadController::uploadImageHtml();
140 }
141 // collage d'une image (code base64 dans le presse-papier) non encapsulée dans du HTML
142 elseif($request->query->get('action') === 'upload_image_base64'){
143 ImageUploadController::uploadImageBase64();
144 }
145
146
147 /* -- requêtes spécifiques au calendrier -- */
148 elseif($request->query->get('action') === 'new_event'){
149 CalendarController::newEvent($json, $entityManager);
150 }
151 elseif($request->query->get('action') === 'update_event'){
152 CalendarController::updateEvent($json, $entityManager);
153 }
154 elseif($request->query->get('action') === 'remove_event'){
155 CalendarController::removeEvent($json, $entityManager);
156 }
157 else{
158 echo json_encode(['success' => false]);
159 die;
160 }
161 }
162
163 /* -- site entier (header, footer, favicon) -- */
164 elseif($request->query->has('head_foot_text')){
165 HeadFootController::setTextData($entityManager, $request->query->get('head_foot_text'), $json);
166 }
167 elseif($request->query->has('head_foot_social_check')){
168 HeadFootController::displaySocialNetwork($entityManager, $request->query->get('head_foot_social_check'), $json);
169 }
170
171 /* -- page Menu et chemins -- */
172 elseif(isset($_GET['menu_edit']))
173 {
174 // ne suit pas la règle, faire ça dans un contrôleur?
175 Model::$menu = new Menu($entityManager); // récupération des données
176
177 // flèche gauche <=: position = position du parent + 1, parent = grand-parent, recalculer les positions
178 if($_GET['menu_edit'] === 'move_one_level_up' && isset($json['id'])){
179 MenuAndPathsController::MoveOneLevelUp($entityManager, $json);
180 }
181 // flèche droite =>: position = nombre d'éléments de la fraterie + 1, l'élément précédent devient le parent
182 elseif($_GET['menu_edit'] === 'move_one_level_down' && isset($json['id'])){
183 MenuAndPathsController::MoveOneLevelDown($entityManager, $json);
184 }
185 elseif($_GET['menu_edit'] === 'switch_positions' && isset($json['id1']) && isset($json['id2'])){
186 MenuAndPathsController::switchPositions($entityManager, $json);
187 }
188 elseif($_GET['menu_edit'] === 'display_in_menu' && isset($json['id']) && isset($json['checked'])){
189 MenuAndPathsController::displayInMenu($entityManager, $json);
190 }
191 elseif($_GET['menu_edit'] === 'url_edit' && isset($json['id']) && isset($json['field']) && isset($json['input_data'])){
192 MenuAndPathsController::editUrl($entityManager, $json);
193 }
194 }
195
196 /* -- mode Modification d'une page -- */
197 // partie "page"
198 elseif(isset($_GET['page_edit']))
199 {
200 // titre de la page
201 if($_GET['page_edit'] === 'page_title'){
202 PageManagementController::setPageTitle($entityManager, $json);
203 }
204 // description dans les métadonnées
205 elseif($_GET['page_edit'] === 'page_description'){
206 PageManagementController::setPageDescription($entityManager, $json);
207 }
208 }
209
210 // partie "blocs"
211 elseif($request->query->has('bloc_edit'))
212 {
213 if($request->query->get('bloc_edit') === 'rename_page_bloc'){
214 PageManagementController::renameBloc($entityManager, $json);
215 }
216 elseif($request->query->get('bloc_edit') === 'switch_blocs_positions'){
217 PageManagementController::SwitchBlocsPositions($entityManager, $json);
218 }
219 elseif($request->query->get('bloc_edit') === 'change_articles_order'){
220 PageManagementController::changeArticlesOrder($entityManager, $json);
221 }
222 elseif($request->query->get('bloc_edit') === 'change_presentation'){
223 PageManagementController::changePresentation($entityManager, $json);
224 }
225 elseif($request->query->get('bloc_edit') === 'change_cols_min_width'){
226 PageManagementController::changeColsMinWidth($entityManager, $json);
227 }
228 elseif($request->query->get('bloc_edit') === 'change_pagination_limit'){
229 PageManagementController::changePaginationLimit($entityManager, $json);
230 }
231 }
232 }
233
234 // upload avec FormData
235 elseif(strpos($_SERVER['CONTENT_TYPE'], 'multipart/form-data') !== false)
236 {
237 // dans tinymce avec le plugin (bouton "insérer une image" de l'éditeur ou glisser-déposer)
238 if($request->query->has('action') && $request->query->get('action') === 'upload_image_tinymce'){
239 ImageUploadController::imageUploadTinyMce();
240 }
241 // dans tinymce, des quatre méthodes: bouton "link", drag & drop, html, base64
242 elseif($request->query->has('action') && $request->query->get('action') === 'upload_file_tinymce'){
243 FileUploadController::fileUploadTinyMce();
244 }
245 elseif($request->query->has('head_foot_image')){
246 HeadFootController::uploadAsset($entityManager, $request->query->get('head_foot_image'));
247 }
248 }
249
250 // requêtes XMLHttpRequest
251 elseif(isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest')
252 {
253 //echo "requête XMLHttpRequest reçue par le serveur";
254 echo json_encode(['success' => false]); // noyer le poisson en laissant penser que le site gère les requêtes XHR
255 die;
256 }
257
258 /* -- envoi formulaire HTML -- */
259 elseif($_SERVER['CONTENT_TYPE'] === 'application/x-www-form-urlencoded')
260 {
261 if($request->query->has('action') && $request->query->get('action') === 'delete_article' && isset($_GET['id'])){
262 $response = ArticleController::deleteArticle($entityManager, $_GET); // version formulaire
263 }
264
265 /* -- nouvelle page -- */
266 elseif(isset($_POST['page_name']) && $_POST['page_name'] !== null
267 && isset($_POST['page_name_path']) && $_POST['page_name_path'] !== null
268 && isset($_POST['page_location']) && $_POST['page_location'] !== null
269 && isset($_POST['page_description']) && $_POST['page_description'] !== null
270 && isset($_POST['new_page_hidden']) && $_POST['new_page_hidden'] === '')
271 {
272 PageManagementController::newPage($entityManager, $_POST);
273 }
274
275 /* -- suppression d'une page -- */
276 elseif(isset($_POST['page_id']) && $_POST['page_id'] !== null
277 && isset($_POST['submit_hidden']) && $_POST['submit_hidden'] === '')
278 {
279 PageManagementController::deletePage($entityManager);
280 }
281
282
283 /* -- mode Modification d'une page -- */
284
285 // modification du chemins en snake_case
286 elseif(isset($_POST['page_menu_path']) && $_POST['page_menu_path'] !== null
287 && isset($_POST['page_id']) && $_POST['page_id'] !== null
288 && isset($_POST['page_name_path_hidden']) && $_POST['page_name_path_hidden'] === '')
289 {
290 PageManagementController::updatePageMenuPath($entityManager);
291 }
292 // ajout d'un bloc dans une page
293 elseif(isset($_POST['bloc_title']) && $_POST['bloc_title'] !== null
294 && isset($_POST['bloc_select']) && $_POST['bloc_select'] !== null
295 && isset($_POST['bloc_title_hidden']) && $_POST['bloc_title_hidden'] === '') // contrôle anti-robot avec input hidden
296 {
297 PageManagementController::addBloc($entityManager);
298 }
299 // suppression d'un bloc de page
300 elseif(isset($_POST['delete_bloc_id']) && $_POST['delete_bloc_id'] !== null
301 && isset($_POST['delete_bloc_hidden']) && $_POST['delete_bloc_hidden'] === '') // contrôle anti-robot avec input hidden
302 {
303 PageManagementController::deleteBloc($entityManager);
304 }
305
306
307 /* -- page Menu et chemins -- */
308
309 // création d'une entrée de menu avec une URL
310 elseif(isset($_POST["label_input"]) && isset($_POST["url_input"]) && isset($_POST["location"])){
311 MenuAndPathsController::newUrlMenuEntry($entityManager);
312 }
313 // suppression d'une entrée de menu avec une URL
314 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
315 MenuAndPathsController::deleteUrlMenuEntry($entityManager);
316 }
317
318
319 /* -- page Mon compte -- */
320 elseif($request->query->has('action') && $request->query->get('action') === 'update_username')
321 {
322 UserController::updateUsername($entityManager);
323 }
324 elseif($request->query->has('action') && $request->query->get('action') === 'update_password')
325 {
326 UserController::updatePassword($entityManager);
327 }
328
329 // redirection page d'accueil
330 else{
331 header("Location: " . new URL(['error' => 'paramètres inconnus']));
332 die;
333 }
334 }
335 // POST admin ne matchant pas
336 else{
337 echo json_encode(['success' => false]);
338 die;
339 }
340 }
341 // POST non admin ne matchant pas
342 else{
343 echo json_encode(['success' => false]);
344 die;
345 }
346}
347
348// méthode inconnue
349else{
350 header("Location: " . new URL(['error' => 'tu fais quoi là mec?']));
351 die;
352}
353
354
355
356/* -- utilisation de la réponse -- */
357if(isset($response)){
358 // 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
359 if($request->isMethod('GET') && $response->getStatusCode() == 302){ // 302 redirection temporaire
360 header('Location: ' . new URL(['page' => $_GET['from'] ?? '']));
361 }
362 // redirection après traitement de formulaires HTTP
363 elseif($request->getMethod() === 'POST' && $_SERVER['CONTENT_TYPE'] === 'application/x-www-form-urlencoded'){
364 $response_data = json_decode(($response)->getContent(), true);
365 $url = new URL(['page' => $_GET['from'] ?? '']);
366 $url->addParams(['success' => $response_data['success'], 'message' => $response_data['message']]);
367 header('Location: ' . $url);
368 }
369 // affichage d'une page OU requête AJAX
370 else{
371 $response->send();
372 }
373}
374// pas utilisation de RESPONSE (cas destiné à disparaître)
375else{
376 if($request->getMethod() === 'POST' && $_SERVER['CONTENT_TYPE'] === 'application/x-www-form-urlencoded'){
377 header("Location: " . new URL(['error' => 'erreur côté serveur']));
378 }
379 else{
380 http_response_code(500);
381 echo "erreur côté serveur";
382 }
383}
384//die; // inutile \ No newline at end of file
diff --git a/src/service/session.php b/src/service/session.php
new file mode 100644
index 0000000..57f2143
--- /dev/null
+++ b/src/service/session.php
@@ -0,0 +1,86 @@
1<?php
2// src/service/session.php
3
4// à voir si c'est mieux avec:
5//use Symfony\Component\HttpFoundation\Session\Session;
6
7
8ini_set('session.cookie_samesite', 'Strict');
9ini_set('session.cookie_httponly', 'On');
10ini_set('session.use_strict_mode', 'On');
11ini_set('session.cookie_secure', 'On');
12session_start();
13validateSession($entityManager);
14
15// note: session_regenerate_id(true) se trouve dans UserController::connect
16
17function validateSession($entityManager): void
18{
19 if(defined('IS_ADMIN')){
20 return;
21 }
22
23 $is_admin = false;
24
25 if(isset($_SESSION['user']['id'])){
26 $user = UserController::getUserById($_SESSION['user']['id'], $entityManager);
27
28 // visiteur normal
29 if(!$user){
30 session_unset();
31 session_destroy();
32 header('Location: ' . new URL(['message' => 'session_invalide']));
33 die;
34 }
35
36 // MAJ de la session avec CERTAINES données
37 $_SESSION['user']['username'] = $user->getLogin();
38 $_SESSION['user']['role'] = $user->getRole();
39
40 $is_admin = $user->getRole() === 'admin';
41 }
42
43 define('IS_ADMIN', $is_admin);
44
45 // si on a un jour besoin d'une variable globale au lieu d'une constante
46 //$GLOBALS['is_admin'] = $is_admin; // version modifiable 1
47 /*function isAdmin(): bool { // version modifiable 2
48 return $_SESSION['user']['role'] ?? null === 'admin';
49 }*/
50
51
52 // => système de cache à ajouter pour ne pas lire la BDD à chaque fois
53 //remplacer ce qui est en haut
54 /*$user = $_SESSION['user'] ?? null;
55 if (!$user) {
56 // visiteur
57 }
58 // Vérification périodique (ex: toutes les 5 minutes)
59 if (time() - $user['last_check'] > 300) {
60 $user = UserController::getUserById($user['id'], $entityManager);
61 if (!$user) {
62 session_destroy();
63 header('Location: /login.php');
64 exit;
65 }
66 // cache pour ne pas avoir à lire la BDD à chaque page
67 $_SESSION['user'] = [
68 'id' => $user['id'],
69 'role' => $user['role'],
70 'username' => $user['username'],
71 'last_check' => time()
72 ];
73 $user = $_SESSION['user'];
74 }
75 $is_admin = ($user['role'] === 'admin');*/
76
77
78 // améliorations possibles: ajouter expiration automatique + protection contre vol de session (IP / user-agent) sans casser ton app.
79}
80
81// nettoyage complet
82/*function cleanSession(){
83 unset($_SESSION['user']); // mémoire vive
84 session_destroy(); // fichier côté serveur
85 setcookie('PHPSESSID', '', time() - 86400, '/'); // cookie de session
86}*/ \ No newline at end of file