aboutsummaryrefslogtreecommitdiff
path: root/src/service
diff options
context:
space:
mode:
Diffstat (limited to 'src/service')
-rw-r--r--src/service/Backup.php233
-rw-r--r--src/service/Captcha.php55
-rw-r--r--src/service/Config.php88
-rw-r--r--src/service/EmailService.php104
-rw-r--r--src/service/FormValidation.php214
-rw-r--r--src/service/Installation.php107
-rw-r--r--src/service/Router.php376
-rw-r--r--src/service/Security.php110
-rw-r--r--src/service/URL.php88
-rw-r--r--src/service/User.php23
-rw-r--r--src/service/UserDataService.php46
-rw-r--r--src/service/session.php90
12 files changed, 0 insertions, 1534 deletions
diff --git a/src/service/Backup.php b/src/service/Backup.php
deleted file mode 100644
index 2ee69c1..0000000
--- a/src/service/Backup.php
+++ /dev/null
@@ -1,233 +0,0 @@
1<?php
2// src/service/Backup.php
3
4declare(strict_types=1);
5
6use Doctrine\ORM\EntityManager;
7use Symfony\Component\Process\Process; // protection injection dans le shell
8
9class Backup
10{
11 static public string $backup_dir = '../var/backups';
12 static private int $amount_to_keep = 20;
13
14 static public function mySQLdump(EntityManager $entityManager, string $type): string
15 {
16 $date = new DateTime;
17 $file_path = self::$backup_dir . '/' . Config::$database . '_' . $date->format('Y-m-d') . '_' . $type . '.sql';
18
19 // les versions de mysql sont comme ci: 8.0.36
20 // celles de mariadb sont comme ça: 10.11.6-MariaDB
21 $version = $entityManager->getConnection()->fetchOne('SELECT VERSION()');
22 $engine = stripos($version, 'mariadb') !== false ? 'mariadb-dump' : 'mysqldump';
23
24 $tmp = tempnam('../var', 'tmp_db_codes_'); // crée un fichier avec un nom aléatoire et des droits 600 (concurrence)
25 // attention, si le dossier de destination n'est pas disponible, le fichier est placé avec les fichiers temporaires
26 file_put_contents($tmp,
27 "[client]\n
28 user=" . Config::$user . "\n
29 password=" . Config::$password . "\n
30 host=" . Config::$db_host . "\n");
31
32 $command = new Process([
33 $engine,
34 '--defaults-extra-file=' . $tmp, // pour ne pas enregistrer les codes dans l'historique de la console ou dans les processus de l'OS
35 '--single-transaction',
36 '--quick', // évite d'exploser la RAM si beaucoup de données
37 '--result-file=' . $file_path,
38 Config::$database
39 ]);
40
41 try{
42 // unlink et chmod permettent que le serveur et l'utilisateur appelant bin/mysqldump.php réussissent
43 if(file_exists($file_path)){
44 unlink($file_path);
45 }
46 $command->mustRun(); // comme run() mais lance une ProcessFailedException
47
48 //$file_path = self::gzipCompress($file_path); // '.gz' ajouté à la fin
49
50 chmod($file_path, 0666);
51 return $file_path;
52 }
53 finally{
54 // exécuté même quand situé après "return"
55 unlink($tmp);
56 self::cleanBackups();
57 }
58 }
59
60 // compression gzip (gros gain de place sur le serveur), nécessite l'extension zlib
61 static public function gzipCompress(string $file_path): string
62 {
63 try{
64 file_put_contents(
65 $file_path . '.gz',
66 gzencode(file_get_contents($file_path), 5), // plus rapide que 9 et taille identique d'après mes essais
67 );
68 unlink($file_path);
69 $file_path .= '.gz';
70 }
71 finally{
72 return $file_path;
73 }
74 }
75
76 static public function getBackupList(): array
77 {
78 $files = scandir(Backup::$backup_dir); // affiche un warning si échoue (à cacher en prod)
79 if(!$files){
80 throw new RuntimeException("Le serveur a rencontré une erreur:<br>Accès aux backups impossible faute de permissions.");
81 }
82
83 $backup_array = [];
84 foreach($files as $file){
85 if($file[0] === '.'){
86 continue;
87 }
88 $backup_array[] = $file;
89 }
90 return $backup_array;
91 }
92
93 static public function cleanBackups(): void
94 {
95 $files = glob(self::$backup_dir . '/*.sql');
96 //usort($files, fn($a, $b) => filemtime($b) <=> filemtime($a)); // filemtime = date de dernière modification
97 arsort($files);
98
99 // tri par nom de BDD puis par date
100 $sorted_files = [];
101 $list_by_database = []; // pour le nettoyage 2
102 foreach($files as $file){
103 $exploded = explode('_', basename($file));
104 $sorted_files[$exploded[0]][$exploded[1]][] = $file;
105 $list_by_database[$exploded[0]][] = $file;
106 }
107
108 $date = new DateTime;
109 foreach($sorted_files as $db_name => $from_one_database){
110 // on garde une "quantité à garder" par BDD
111 if(count($from_one_database) > self::$amount_to_keep){
112 // nettoyage 1
113 foreach($from_one_database as $date_key => $with_same_date){
114 // pas touche à aujourd'hui
115 if($date_key != $date->format('Y-m-d')){
116 self::cleanBackupsByPriority($with_same_date);
117 }
118 }
119 // nettoyage 2
120 $files_to_delete = array_slice($list_by_database[$db_name], self::$amount_to_keep);
121 foreach($files_to_delete as $file){
122 unlink($file);
123 }
124 }
125 }
126 }
127
128 // conserver un seul backup par jour choisi dans cet ordre de préférence: console => before-restore => download => auto
129 // cet ordre correspond à: volonté de l'utilisateur => état du jour avant changement => volonté de changement => automatique sans contrôle
130 static private function cleanBackupsByPriority(array $files): void
131 {
132 $priorities = [
133 'console' => 1,
134 'before-restore' => 2,
135 'uploaded' => 3,
136 'auto' => 4,
137 ];
138 $best_priority = PHP_INT_MAX;
139
140 // recherche du fichier à conserver
141 $to_keep = null;
142 foreach($files as $file){
143 foreach($priorities as $keyword => $priority){
144 if(str_contains(basename($file), $keyword) && $priority < $best_priority){
145 $best_priority = $priority;
146 $to_keep = $file;
147 break;
148 }
149 }
150 }
151 // suppression des autres
152 foreach($files as $file){
153 if($file !== $to_keep){
154 unlink($file);
155 }
156 }
157 }
158
159 static public function restoreDatabase(EntityManager $entityManager, string $file_name, array $exclusion_list = []): void
160 {
161 // création d'un backup de sécurité non écrasable
162 $date = new DateTime;
163 if(!file_exists(self::$backup_dir . '/' . Config::$database . '_' . $date->format('Y-m-d') . '_before-restore.sql')){
164 Backup::mySQLdump($entityManager, 'before-restore');
165 }
166
167 $version = $entityManager->getConnection()->fetchOne('SELECT VERSION()');
168 $engine = stripos($version, 'mariadb') !== false ? 'mariadb' : 'mysql';
169
170 $tables = self::getDBListOfTables($entityManager, $exclusion_list); // choisir les tables à effacer et restaurer, exclure "user"
171
172 $tmp_db_codes = tempnam('../var', 'tmp_db_codes_'); // crée un fichier avec un nom aléatoire et des droits 600 (concurrence)
173 file_put_contents($tmp_db_codes,
174 "[client]\n
175 user=" . Config::$user . "\n
176 password=" . Config::$password . "\n
177 host=" . Config::$db_host . "\n");
178
179 //$file_name = self::gzipExtract($file_name); // '.gz' ajouté à la fin
180
181 $command = new Process([
182 $engine, // mariadb ou mysql
183 '--defaults-extra-file=' . $tmp_db_codes, // pour ne pas enregistrer les codes dans l'historique de la console ou dans les processus de l'OS
184 Config::$database
185 ]);
186 $command->setInput(file_get_contents(Backup::$backup_dir . '/' . $file_name)); // l'entrée <
187
188 // utiliser une liste de tables à exclure dynamique
189 try{
190 // tout effacer
191 $tables_with_backquotes = array_map(fn($t) => '`' . $t . '`', $tables);
192 if(!empty($tables_with_backquotes)){
193 $sql = "SET FOREIGN_KEY_CHECKS=0; DROP TABLE " . implode(', ', $tables_with_backquotes) . "; SET FOREIGN_KEY_CHECKS=1;";
194 $entityManager->getConnection()->executeStatement($sql);
195 }
196
197 // copie des tables à ne pas restaurer
198 foreach($exclusion_list as $excluded){
199 $entityManager->getConnection()->executeStatement('RENAME TABLE `' . TABLE_PREFIX . $excluded . '` TO `' . TABLE_PREFIX . $excluded . '_dont_touch`;');
200 }
201
202 // restaurer
203 $command->mustRun(); // comme run() mais lance une ProcessFailedException
204
205 // remettre table user comme avant
206 foreach($exclusion_list as $excluded){
207 $entityManager->getConnection()->executeStatement('DROP TABLE `' . TABLE_PREFIX . $excluded . '`;');
208 $entityManager->getConnection()->executeStatement('RENAME TABLE `' . TABLE_PREFIX . $excluded . '_dont_touch` TO `' . TABLE_PREFIX . $excluded . '`;');
209 }
210 }
211 finally{
212 // exécuté même quand situé après "return"
213 unlink($tmp_db_codes);
214 }
215 }
216
217 static private function getDBListOfTables(EntityManager $entityManager, array $exclusion_list = []): array
218 {
219 $tables = $entityManager->getConnection()->createSchemaManager()->listTableNames();
220 foreach($tables as $key => $elem){
221 foreach($exclusion_list as $excluded){
222 if(!is_string($excluded)){ // cas pas du tout censé arriver!
223 throw new LogicException("Un nom de table doit etre une chaîne de caractères.");
224 }
225 if($elem === TABLE_PREFIX . $excluded){
226 unset($tables[$key]);
227 break; // petite optimisation
228 }
229 }
230 }
231 return $tables;
232 }
233} \ No newline at end of file
diff --git a/src/service/Captcha.php b/src/service/Captcha.php
deleted file mode 100644
index d57f912..0000000
--- a/src/service/Captcha.php
+++ /dev/null
@@ -1,55 +0,0 @@
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
deleted file mode 100644
index 7077e01..0000000
--- a/src/service/Config.php
+++ /dev/null
@@ -1,88 +0,0 @@
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 // envoi 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
27 // destination e-mails
28 static public string $email_from = ''; // correspondant affiché dans la boite de réception
29 static public string $email_from_name = 'site web';
30 static public string $email_dest = ''; // destinataire affiché dans la boite de réception
31 static public string $email_dest_name = 'moi';
32
33 // copier dans ce tableau les variables contenant des chemins
34 static private array $path_vars = [];
35
36 static public function load(string $file_path): void
37 {
38 if(file_exists($file_path))
39 {
40 // ce serait bien de gérer aussi les fichiers corrompus?
41 $raw_data = parse_ini_file($file_path);
42 self::hydrate($raw_data);
43 }
44 else
45 {
46 echo "<p>Le fichier config/config.ini n'existe pas ou n'est pas lisible.</p>";
47 }
48 define('TABLE_PREFIX', self::$table_prefix);
49 }
50
51 // renseigner les variables internes de Config
52 static private function hydrate(array $raw_data): void
53 {
54 foreach($raw_data as $field => $value)
55 {
56 if($value != '') // valeur par défaut
57 {
58 if(isset(self::$$field)) // le champ existe dans Config
59 {
60 // problème du slash à la fin du nom d'un dossier
61 $value = self::slashAtEndOfPath($field, $value);
62 self::$$field = $value;
63 }
64 else
65 {
66 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";
67 }
68 }
69 /*else
70 {
71 echo "debug: le champ " . $field . " est vide, la valeur par défaut " . self::$$field . " sera utilisée.\n";
72 }*/
73 }
74 }
75
76
77 // pour que les chemins finissent toujours par un /
78 static private function slashAtEndOfPath(string $field, string $value): string
79 {
80 foreach(self::$path_vars as $item)
81 {
82 if($field === $item){
83 return !str_ends_with($value, '/') ? $value . '/' : $value;
84 }
85 }
86 return $value;
87 }
88}
diff --git a/src/service/EmailService.php b/src/service/EmailService.php
deleted file mode 100644
index 1579bdb..0000000
--- a/src/service/EmailService.php
+++ /dev/null
@@ -1,104 +0,0 @@
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\EmailForm;
11
12class EmailService
13{
14 const KEEP_EMAILS_DEFAULT = false;
15
16 static public function send(EntityManager $entityManager, EmailForm $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
26 $email_from = $form_data->getData()['email_from'] ?? Config::$email_from; // adresse de l'expéditeur affichée
27 $email_from = empty($email_from) ? $smtp_username : $email_from; // si vide
28 $email_from_name = $form_data->getData()['email_from_name'] ?? Config::$email_from_name; // = nom de l'expéditeur affiché
29 $email_dest = $form_data->getData()['email_dest'] ?? Config::$email_dest;
30 $email_dest_name = $form_data->getData()['email_dest_name'] ?? Config::$email_dest_name; // = destinataire formulaire
31
32 try{
33 // Paramètres du serveur
34 $mail->isSMTP();
35 $mail->Host = $smtp_host;
36 $mail->SMTPAuth = true;
37 $mail->Port = 25;
38
39 if($mail->SMTPAuth){
40 $mail->Username = $smtp_username; // e-mail
41 $mail->Password = $smtp_password;
42 $mail->SMTPSecure = $smtp_secure; // tls (starttls) ou ssl (smtps)
43 if($mail->SMTPSecure === 'tls'){
44 $mail->Port = 587;
45 }
46 elseif($mail->SMTPSecure === 'ssl'){
47 $mail->Port = 465;
48 }
49 }
50 //var_dump($mail->smtpConnect());die; // test de connexion
51
52 // Expéditeur et destinataire
53 // $email_from, $email_from_name et $email_dest_name sont modifiables uniquement dans le config.ini pour l'instant
54 $mail->setFrom(strtolower($email_from), $email_from_name);
55 $mail->addAddress(strtolower($email_dest), $email_dest_name);
56
57 // Contenu
58 $mail->isHTML(true);
59 if($test_email){
60 $mail->Subject = "TEST d'un envoi d'e-mail depuis le site web";
61 }
62 else{
63 $mail->Subject = 'Message envoyé par: ' . $name . ' (' . $email . ') depuis le site web';
64 }
65 $mail->Body = $message;
66 $mail->AltBody = $message;
67
68 $mail->send();
69
70 // copie en BDD
71 if(!$test_email && ($form_data->getData()['keep_emails'] ?? self::KEEP_EMAILS_DEFAULT)){
72 $db_email = new Email($name, $email, $email_dest, $message, $form_data);
73 $entityManager->persist($db_email);
74 self::updateLastContactDate($entityManager, $email);
75 $entityManager->flush();
76 }
77
78 return true;
79 }
80 catch(Exception $e){
81 echo "Le message n'a pas pu être envoyé. Erreur : {$e} <br> {$mail->ErrorInfo}";
82 return false;
83 }
84 }
85
86 static public function updateLastContactDate(EntityManager $entityManager, string $sender): void
87 {
88 foreach($entityManager->getRepository('App\Entity\Email')->findAll() as $email){
89 $email->getSenderAddress() === $sender ? $email->updateLastContactDate() : null;
90 }
91 }
92
93 // peut être appelée par bin/clean_emails_cron.php
94 static public function cleanEmails(EntityManager $entityManager): void
95 {
96 $emails = $entityManager->getRepository('App\Entity\Email')->findAll();
97 foreach($emails as $email){
98 if($email->getDeletionDate() < new \DateTime()){
99 $entityManager->remove($email);
100 }
101 }
102 $entityManager->flush();
103 }
104} \ No newline at end of file
diff --git a/src/service/FormValidation.php b/src/service/FormValidation.php
deleted file mode 100644
index 9d0b838..0000000
--- a/src/service/FormValidation.php
+++ /dev/null
@@ -1,214 +0,0 @@
1<?php
2// src/service/FormValidation.php
3//
4// pattern strategy simplifié, permet évolution future vers le vrai pattern
5
6declare(strict_types=1);
7
8// classe spéciale permettant de bénéficier du typage de classe au lieu de passer une chaîne à FormValidation
9enum ValidationStrategy {
10 // bloc formulaire de contact
11 case EmailSend;
12 case EmailParam;
13 // formulaires pages spéciales
14 case CreateUser;
15 case Connection;
16 case UsernameUpdate;
17 case PasswordUpdate;
18
19 public function method(): string
20 {
21 return match($this){
22 self::EmailSend => 'emailSendStrategy',
23 self::EmailParam => 'emailParamStrategy',
24 self::CreateUser => 'createUserStrategy',
25 self::Connection => 'connectionStrategy',
26 self::UsernameUpdate => 'usernameUpdateStrategy',
27 self::PasswordUpdate => 'passwordUpdateStrategy'
28 };
29 }
30}
31
32class FormValidation
33{
34 private array $data; // tableau associatif (probablement $_POST)
35 private Closure $callStrategy;
36 private ?string $error;
37
38 public function __construct(array $data, ValidationStrategy $strategy){
39 $this->data = $data;
40 $method = $strategy->method();
41 $this->callStrategy = fn() => $this->$method();
42 }
43
44 public function validate(): bool
45 {
46 $this->error = ($this->callStrategy)(); // appel de la callback
47 return empty($this->errors);
48 }
49
50 public function getError(): string
51 {
52 return $this->error;
53 }
54
55 public function getField(string $field): string
56 {
57 return $this->data[$field] ?? '';
58 }
59
60 // méthodes de validation
61 private function captchaValidate(bool $clean_session = true): ?string
62 {
63 $captcha_solution = (isset($_SESSION['captcha']) && is_int($_SESSION['captcha'])) ? $_SESSION['captcha'] : 0;
64 $captcha_try = isset($this->data['captcha']) ? Captcha::controlInput($this->data['captcha']) : 0;
65 if($clean_session){
66 unset($_SESSION['captcha']);
67 }
68
69 if($captcha_try == 0){
70 return 'error_non_valid_captcha';
71 }
72 elseif($captcha_solution == 0){ // ne peut pas arriver, si?
73 return 'captcha_server_error';
74 }
75 elseif($captcha_try !== $captcha_solution){
76 return 'bad_solution_captcha';
77 }
78 return null;
79 }
80
81 // erreurs à la création des mots de passe
82 static private function removeSpacesTabsCRLF(string $chaine): string
83 {
84 $cibles = [' ', "\t", "\n", "\r"]; // doubles quotes !!
85 return(str_replace($cibles, '', $chaine));
86 }
87
88
89 // stratégies
90 private function emailSendStrategy(): ?string
91 {
92 if($error = $this->captchaValidate(false)){
93 return $error;
94 }
95
96 if(empty($this->data['name']) || empty($this->data['email']) || empty($this->data['message'])
97 || !isset($this->data['hidden']) || !empty($this->data['hidden'])){
98 return 'missing_fields';
99 }
100
101 elseif(!filter_var(trim($this->data['email']), FILTER_VALIDATE_EMAIL)){
102 return 'bad_email_address';
103 }
104
105 $this->data['name'] = htmlspecialchars(trim($this->data['name']));
106 $this->data['email'] = htmlspecialchars(trim($this->data['email']));
107 $this->data['message'] = htmlspecialchars($this->data['message']);
108
109 return null;
110 }
111 private function emailParamStrategy(): ?string
112 {
113 if(empty($this->data['id']) || empty($this->data['what_param']) || empty($this->data['value'])
114 || !isset($this->data['hidden']) || !empty($this->data['hidden'])){
115 return 'missing_fields';
116 }
117
118 if(!in_array($this->data['what_param'], ['smtp_host', 'smtp_secure', 'smtp_username', 'smtp_password', 'email_dest'])){
119 return 'unknown_parameter';
120 }
121 elseif($this->data['what_param'] === 'smtp_username' || $this->data['what_param'] === 'email_dest'){
122 if(!filter_var($this->data['value'], FILTER_VALIDATE_EMAIL)){
123 return 'invalide_email_address';
124 }
125 }
126 return null;
127 }
128 private function createUserStrategy(): ?string
129 {
130 if($error = $this->captchaValidate()){
131 return $error;
132 }
133
134 // test mauvais paramètres
135 if(empty($this->data['login']) || empty($this->data['password']) || empty($this->data['password_confirmation'])
136 || !isset($this->data['create_user_hidden']) || !empty($this->data['create_user_hidden']))
137 {
138 return 'bad_login_or_password';
139 }
140
141 if($this->data['password'] !== $this->data['password_confirmation']){
142 return 'different_passwords';
143 }
144
145 if($this->data['login'] !== self::removeSpacesTabsCRLF(htmlspecialchars($this->data['login']))
146 || $this->data['password'] !== self::removeSpacesTabsCRLF(htmlspecialchars($this->data['password']))){
147 return 'forbidden_characters';
148 }
149 return null;
150 }
151 private function connectionStrategy(): ?string
152 {
153 if($error = $this->captchaValidate()){
154 return $error;
155 }
156
157 if(empty($this->data['login']) || empty($this->data['password'])
158 || !isset($this->data['connection_hidden']) || !empty($this->data['connection_hidden']))
159 {
160 return 'bad_login_or_password';
161 }
162 return null;
163 }
164 private function usernameUpdateStrategy(): ?string
165 {
166 if($error = $this->captchaValidate()){
167 return $error;
168 }
169
170 if(empty($this->data['login']) || empty($this->data['password']) || empty($this->data['new_login'])
171 || !isset($this->data['modify_username_hidden']) || !empty($this->data['modify_username_hidden']))
172 {
173 return 'bad_login_or_password';
174 }
175
176 $new_login = self::removeSpacesTabsCRLF(htmlspecialchars($this->data['new_login']));
177 if($new_login !== $this->data['new_login']){
178 return 'forbidden_characters';
179 }
180
181 if($this->data['login'] !== $_SESSION['user']['username']){
182 return 'bad_login_or_password';
183 }
184 if($this->data['login'] === $new_login){
185 return 'same_username_as_before';
186 }
187 return null;
188 }
189 private function passwordUpdateStrategy(): ?string
190 {
191 if($error = $this->captchaValidate()){
192 return $error;
193 }
194
195 if(empty($this->data['login']) || empty($this->data['password']) || empty($this->data['new_password'])
196 || !isset($this->data['modify_password_hidden']) || !empty($this->data['modify_password_hidden']))
197 {
198 return 'bad_login_or_password';
199 }
200
201 $new_password = self::removeSpacesTabsCRLF(htmlspecialchars($this->data['new_password']));
202 if($new_password !== $this->data['new_password']){
203 return 'forbidden_characters';
204 }
205
206 if($this->data['login'] !== $_SESSION['user']['username']){
207 return 'bad_login_or_password';
208 }
209 if($this->data['password'] === $new_password){
210 return 'same_password_as_before';
211 }
212 return null;
213 }
214} \ No newline at end of file
diff --git a/src/service/Installation.php b/src/service/Installation.php
deleted file mode 100644
index f7de2cf..0000000
--- a/src/service/Installation.php
+++ /dev/null
@@ -1,107 +0,0 @@
1<?php
2// src/service/Installation.php
3
4declare(strict_types=1);
5
6class Installation
7{
8 static public function phpDependancies(): void
9 {
10 $flag = false;
11 $extensions = ['pdo_mysql', 'mbstring', 'ctype', 'json', 'tokenizer', 'imagick', 'zip']; // les 5 premières sont pour doctrine
12 // ajouter plus tard zlib pour la compression des backups
13 foreach($extensions as $extension){
14 if(!extension_loaded($extension)){
15 echo("<p>L'extension <b>" . $extension . "</b> est manquante.</p>");
16 $flag = true;
17 }
18 }
19 if(!class_exists(DOMDocument::class)){ // théoriquement plus fiable que extension_loaded()
20 echo("<p>L'extension <b>dom</b> est manquante.</p>");
21 $flag = true;
22 }
23
24 /*if(!extension_loaded('imagick') && !extension_loaded('gd')){
25 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>");
26 $flag = true;
27 }*/
28 // si imagick n'est pas disponible, essayer gd (reste encore à coder)
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 = 0777;
41
42 $flag = false;
43 if(!file_exists('user_data')){
44 try{
45 mkdir('user_data/');
46 chmod('user_data/', $droits_dossiers);
47 }
48 catch(Exception $e){
49 echo '<p style="color: red;">Le dossier public/user_data introuvable et le serveur n\'a pas la permission de le créer.<br>
50 Pour faire ça bien:<br>sudo -u "serveur web" mkdir /chemin/du/site/public/user_data</p>';
51 echo $e;
52 $flag = true;
53 }
54 }
55 if(!file_exists('../var')){
56 try{
57 mkdir('../var');
58 chmod('../var', $droits_dossiers);
59 }
60 catch(Exception $e){
61 echo $e;
62 $flag = true;
63 }
64 }
65 if(!file_exists('../var/backups')){
66 try{
67 mkdir('../var/backups');
68 chmod('../var/backups', $droits_dossiers); // autoriser à la fois le serveur et les scripts dans bin/
69 }
70 catch(Exception $e){
71 echo $e;
72 $flag = true;
73 }
74 }
75
76 // droits 600 pour celui-ci
77 if(!file_exists('../config/config.ini')){
78 // aide à la création du config.ini
79 echo '<p>Le fichier config/config.ini est introuvable.</p>';
80 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>
81 Un modèle est disponible, il s\'agit du fichier config/config-template.ini</p>
82 <p>Ce fichier a une importance critique. Si vous le pouvez faites en sorte que le serveur en soit le propriétaire et donner lui des droits 600.</p>';
83 $flag = true;
84 }
85 /*else{
86 // propriétaire du fichier
87 if(fileowner('../config/config.ini') != posix_geteuid()){
88 echo "<p>le fichier config/config.ini n'appartient pas au serveur.</p>";
89 }
90 else{
91 // droits du config.ini
92 if(substr(sprintf('%o', fileperms('../config/config.ini')), -4) != 600){
93 echo '<p>Attention, le </p>';
94 //chmod('../config/config.ini', $droits_fichiers);
95 }
96 }
97 }*/
98 if($flag){
99 die;
100 }
101
102 // tester les liens internes
103 //
104
105 // le test de connexion à la BDD est dans le doctrine bootstrap
106 }
107} \ No newline at end of file
diff --git a/src/service/Router.php b/src/service/Router.php
deleted file mode 100644
index 28a6c39..0000000
--- a/src/service/Router.php
+++ /dev/null
@@ -1,376 +0,0 @@
1<?php
2// src/service/router.php
3//
4/* fonctionnement:
5=> 1er test, méthode http GET? POST?
6=> 2ème test, type de contenu:
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/* classes de réponses pour les contrôleurs
14Response($html, Response::HTTP_OK) page html
15JsonResponse(['success' => true, 'data' => $data]) ajax
16RedirectResponse('index.php?page=login') redirection
17BinaryFileResponse($filePath) téléchargement
18StreamedResponse(function () {echo "ligne 1\n";echo "ligne 2\n";}) gros fichier */
19
20// relire ça à l'occaz:
21// https://symfony.com/doc/current/introduction/from_flat_php_to_symfony.html
22
23declare(strict_types=1);
24
25use Symfony\Component\HttpFoundation\Request;
26use Doctrine\ORM\EntityManager;
27use Symfony\Component\HttpFoundation\JsonResponse;
28use Symfony\Component\HttpFoundation\RedirectResponse;
29use Symfony\Component\HttpFoundation\Response;
30
31class Router{
32 private Request $request;
33 private EntityManager $entityManager;
34 private string $route = ''; // défaut page d'accueil
35
36 public function __construct(Request $request, EntityManager $entityManager){
37 $this->request = $request;
38 $this->entityManager = $entityManager;
39
40 if(!User::existUsers($entityManager)){ // table "user" vide
41 $this->route = 'no_user';
42 }
43 }
44
45 public function dispatch(): Response
46 {
47 if($this->request->getMethod() === 'GET'){
48 // table "user" vide
49 if($this->route === 'no_user'){
50 ob_start();
51 require AbstractBuilder::VIEWS_PATH . 'user_create.php';
52 return new Response(ob_get_clean());
53 }
54
55 // bouton déconnexion (méthode GET parce que l'utilisateur ne modifie plus de données à partir de là)
56 if($this->request->query->get('action') === 'deconnection'){
57 return UserController::disconnect(); // retourne un RedirectResponse
58 }
59
60 // articles suivants
61 if($this->request->query->get('fetch') === 'next_articles'){
62 return ArticleController::fetch($this->entityManager, $this->request); // retourne un JsonResponse
63 }
64
65 // données du calendrier
66 // création du calendrier ou changement de dates affichées (boutons flèches mais pas changement de vue)
67 if($this->request->query->get('action') === 'get_events'
68 && $this->request->query->has('start') && $this->request->query->has('end') && empty($this->request->getPayload()->all())){ // getPayload ne récupère pas que des POST
69 return CalendarController::getData($this->entityManager);
70 }
71
72 // pages interdites
73 if(!IS_ADMIN && in_array(CURRENT_PAGE, ['menu_paths', 'new_page', 'user_edit', 'emails', 'maintenance'])){
74 return new RedirectResponse((string)new URL);
75 }
76
77 if(IS_ADMIN){
78 if($this->request->query->get('action') === 'get_mysqldump'){
79 return MaintenanceController::getLastDump($this->entityManager);
80 }
81 if($this->request->query->get('action') === 'get_all_media'){
82 return MaintenanceController::getAllMedia();
83 }
84 }
85
86 // affichage d'une page
87 return ViewController::getWebPage($this->entityManager, $this->request);
88 }
89
90
91 elseif($this->request->getMethod() === 'POST'){
92 /* -- contrôleurs appelables par tout le monde -- */
93
94 // table "user" vide
95 if($this->route === 'no_user'){
96 return UserController::createAdminUser($this->entityManager);
97 }
98
99 // requête JSON avec fetch()
100 if($this->request->headers->get('Content-Type') === 'application/json')
101 {
102 $json = json_decode($this->request->getContent(), true); // = json_decode(file_get_contents('php://input'), true);
103
104 // formulaire de contact
105 if($this->request->query->get('action') === 'send_email'){
106 return ContactFormController::sendVisitorEmail($this->entityManager, $json);
107 }
108 /*else{
109 return new JsonResponse(['success' => false, 'error' => 'tu fais quoi là mec?'], JsonResponse::HTTP_BAD_REQUEST);
110 }*/
111 }
112
113 // envoi formulaire HTML
114 elseif($this->request->headers->get('Content-Type') === 'application/x-www-form-urlencoded'){
115 // tentative de connexion
116 if($this->request->query->get('action') === 'connection'){
117 return UserController::connect($this->entityManager);
118 }
119 /*else{
120 return new RedirectResponse((string)new URL(['error' => 'tu fais quoi là mec?']));
121 }*/
122 }
123
124 if(IS_ADMIN){
125 /* -- requêtes AJAX -- */
126
127 // requêtes JSON avec fetch()
128 if($this->request->headers->get('Content-Type') === 'application/json'){
129 $json = json_decode($this->request->getContent(), true); // = json_decode(file_get_contents('php://input'), true);
130
131 if($this->request->query->has('action')){
132 /* -- manipulation des articles -- */
133 if($this->request->query->get('action') === 'editor_submit' && isset($json['id']) && isset($json['content'])){
134 return ArticleController::editorSubmit($this->entityManager, $json);
135 }
136 elseif($this->request->query->get('action') === 'delete_article' && isset($json['id'])){
137 return ArticleController::deleteArticle($this->entityManager, $this->request); // version AJAX
138 }
139 elseif($this->request->query->get('action') === 'switch_positions' && isset($json['id1']) && isset($json['id2'])){
140 return ArticleController::switchPositions($this->entityManager, $json);
141 }
142 elseif($this->request->query->get('action') === 'date_submit' && isset($json['id']) && isset($json['date'])){
143 return ArticleController::dateSubmit($this->entityManager, $json);
144 }
145
146 switch($this->request->query->get('action')){
147 /* -- bloc Formulaire -- */
148 case 'keep_emails':
149 return ContactFormController::keepEmails($this->entityManager, $json);
150 case 'set_retention_period':
151 return ContactFormController::setEmailsRetentionPeriod($this->entityManager, $json);
152 case 'set_email_param':
153 return ContactFormController::setEmailParam($this->entityManager, $json);
154 case 'test_email':
155 return ContactFormController::sendTestEmail($this->entityManager, $json);
156
157 /* -- page emails -- */
158 case 'delete_email':
159 return ContactFormController::deleteEmail($this->entityManager, $json);
160 case 'toggle_sensitive_email':
161 return ContactFormController::toggleSensitiveEmail($this->entityManager, $json);
162
163 /* -- upload d'image dans tinymce par copier-coller -- */
164 // collage de HTML contenant une ou plusieurs balises <img>
165 case 'upload_image_url':
166 return ImageUploadController::uploadImageHtml();
167 // collage d'une image (code base64 dans le presse-papier) non encapsulée dans du HTML
168 case 'upload_image_base64':
169 return ImageUploadController::uploadImageBase64();
170
171 /* -- requêtes spécifiques au calendrier -- */
172 case 'new_event':
173 return CalendarController::newEvent($json, $this->entityManager);
174 case 'update_event':
175 return CalendarController::updateEvent($json, $this->entityManager);
176 case 'remove_event':
177 return CalendarController::removeEvent($json, $this->entityManager);
178
179 /* -- mode maintenance -- */
180 case 'get_logs':
181 return MaintenanceController::getLogs($this->entityManager);
182 case 'erase_logs':
183 return MaintenanceController::eraseLogs($this->entityManager);
184
185 default:
186 return new JsonResponse(['success' => false, 'error' => 'bad parameters'], JsonResponse::HTTP_BAD_REQUEST);
187 }
188 }
189
190 /* -- site entier (header, footer, favicon) -- */
191 elseif($this->request->query->has('head_foot_text')){
192 return HeadFootController::setTextData($this->entityManager, $this->request->query->get('head_foot_text'), $json);
193 }
194 elseif($this->request->query->has('head_foot_social_check')){
195 return HeadFootController::displaySocialNetwork($this->entityManager, $this->request->query->get('head_foot_social_check'), $json);
196 }
197
198 /* -- page Menu et chemins -- */
199 elseif($this->request->query->has('menu_edit')){
200 // ne suit pas la règle, faire ça dans un contrôleur?
201 Model::$menu = new Menu($this->entityManager); // récupération des données
202
203 // flèche gauche <=: position = position du parent + 1, parent = grand-parent, recalculer les positions
204 if($this->request->query->get('menu_edit') === 'move_one_level_up' && isset($json['id'])){
205 return MenuAndPathsController::MoveOneLevelUp($this->entityManager, $json);
206 }
207 // flèche droite =>: position (léments de a fraterie + 1, l'élément précédent devient le parent
208 elseif($this->request->query->get('menu_edit') === 'move_one_level_down' && isset($json['id'])){
209 return MenuAndPathsController::MoveOneLevelDown($this->entityManager, $json);
210 }
211 elseif($this->request->query->get('menu_edit') === 'switch_positions' && isset($json['id1']) && isset($json['id2'])){
212 return MenuAndPathsController::switchPositions($this->entityManager, $json);
213 }
214 elseif($this->request->query->get('menu_edit') === 'display_in_menu' && isset($json['id']) && isset($json['checked'])){
215 return MenuAndPathsController::displayInMenu($this->entityManager, $json);
216 }
217 elseif($this->request->query->get('menu_edit') === 'url_edit' && isset($json['id']) && isset($json['field']) && isset($json['input_data'])){
218 return MenuAndPathsController::editUrl($this->entityManager, $json);
219 }
220 else{
221 return new JsonResponse(['success' => false, 'error' => 'bad parameters'], JsonResponse::HTTP_BAD_REQUEST); // code 400
222 }
223 }
224
225 /* -- mode Modification d'une page -- */
226 // partie "page"
227 elseif($this->request->query->has('page_edit')){
228 switch($this->request->query->get('page_edit')){
229 case 'page_title':
230 return PageManagementController::setPageTitle($this->entityManager, $json);
231 case 'page_description':
232 return PageManagementController::setPageDescription($this->entityManager, $json);
233 default:
234 return new JsonResponse(['success' => false, 'error' => 'bad parameters'], JsonResponse::HTTP_BAD_REQUEST); // code 400
235 }
236 }
237
238 // partie "blocs"
239 elseif($this->request->query->has('bloc_edit')){
240 switch($this->request->query->get('bloc_edit')){
241 case 'rename_page_bloc':
242 return PageManagementController::renameBloc($this->entityManager, $json);
243 case 'switch_blocs_positions':
244 return PageManagementController::SwitchBlocsPositions($this->entityManager, $json, $this->request->query->get('page'));
245 case 'change_articles_order':
246 return PageManagementController::changeArticlesOrder($this->entityManager, $json);
247 case 'change_presentation':
248 return PageManagementController::changePresentation($this->entityManager, $json);
249 case 'change_cols_min_width':
250 return PageManagementController::changeColsMinWidth($this->entityManager, $json);
251 case 'change_pagination_limit':
252 return PageManagementController::changePaginationLimit($this->entityManager, $json);
253 default:
254 return new JsonResponse(['success' => false, 'error' => 'bad parameters'], JsonResponse::HTTP_BAD_REQUEST); // code 400
255 }
256 }
257
258 else{
259 return new JsonResponse(['success' => false, 'error' => 'bad parameters'], JsonResponse::HTTP_BAD_REQUEST); // code 400
260 }
261 }
262
263 /* -- upload avec FormData OU formulaire HTML avec fichier -- */
264 elseif(str_starts_with($this->request->headers->get('Content-Type'), 'multipart/form-data')){ // = $_SERVER['CONTENT_TYPE']
265 // dans tinymce avec le plugin (bouton "insérer une image" de l'éditeur ou glisser-déposer)
266 if($this->request->query->get('action') === 'upload_image_tinymce'){
267 return ImageUploadController::imageUploadTinyMce();
268 }
269 // dans tinymce, des quatre méthodes: bouton "link", drag & drop, html, base64
270 elseif($this->request->query->get('action') === 'upload_file_tinymce'){
271 return FileUploadController::fileUploadTinyMce();
272 }
273 elseif($this->request->query->has('head_foot_image')){
274 return HeadFootController::uploadAsset($this->entityManager, $this->request->query->get('head_foot_image'));
275 }
276
277 /* -- page Maintenance -- */
278 elseif($this->request->query->get('action') === 'restore_database' && $this->request->request->get('hidden') === ''
279 && $this->request->files->has('uploaded_sql')){
280 return MaintenanceController::downloadSQL($entityManager, $request);
281 }
282 else{
283 // choix ici entre répondre en JSON ou par une redirection, choix du JSON pour pouvoir passer un message
284 return new JsonResponse(['success' => false, 'error' => 'bad parameters'], JsonResponse::HTTP_BAD_REQUEST); // code 400
285 }
286 }
287
288 /* -- formulaire HTML sans fichier -- */
289 elseif($this->request->headers->get('Content-Type') === 'application/x-www-form-urlencoded'){
290 if($this->request->query->get('action') === 'delete_article' && $this->request->query->has('id')){
291 return ArticleController::deleteArticle($this->entityManager, $this->request); // version formulaire
292 }
293
294 /* -- nouvelle page -- */
295 elseif($this->request->request->get('page_name') !== null
296 && $this->request->request->get('page_name_path') !== null
297 && $this->request->request->get('page_location') !== null
298 && $this->request->request->get('page_description') !== null
299 && $this->request->request->get('new_page_hidden') === ''){
300 return PageManagementController::newPage($this->entityManager, $this->request->request);
301 }
302
303 /* -- suppression d'une page -- */
304 elseif($this->request->request->get('page_id') !== null
305 && $this->request->request->get('submit_hidden') === ''){
306 return PageManagementController::deletePage($this->entityManager, $this->request->request->get('page_id'));
307 }
308
309
310 /* -- mode Modification d'une page -- */
311 // modification du chemins en snake_case
312 elseif($this->request->request->get('page_menu_path') !== null
313 && $this->request->request->get('page_id') !== null
314 && $this->request->request->get('page_name_path_hidden') === ''){
315 return PageManagementController::updatePageMenuPath($this->entityManager, $this->request->request->get('page_menu_path'));
316 }
317 // ajout d'un bloc dans une page
318 elseif($this->request->request->get('bloc_title') !== null
319 && $this->request->request->get('bloc_select') !== null
320 && $this->request->request->get('bloc_title_hidden') === ''){ // contrôle anti-robot avec input hidden
321 return PageManagementController::addBloc($this->entityManager, $this->request);
322 }
323 // suppression d'un bloc de page
324 elseif($this->request->request->get('delete_bloc_id') !== null
325 && $this->request->request->get('delete_bloc_hidden') === ''){ // contrôle anti-robot avec input hidden
326 return PageManagementController::deleteBloc($this->entityManager, $this->request);
327 }
328
329
330 /* -- page Menu et chemins -- */
331 // création d'une entrée de menu avec une URL
332 elseif($this->request->request->has("label_input") && $this->request->request->has("url_input") && $this->request->request->has("location")){
333 return MenuAndPathsController::newUrlMenuEntry($this->entityManager);
334 }
335 // suppression d'une entrée de menu avec une URL
336 elseif($this->request->request->has('delete') && $this->request->request->has('x') && $this->request->request->has('y')){ // 2 params x et y sont là parce qu'on a cliqué sur une image
337 return MenuAndPathsController::deleteUrlMenuEntry($this->entityManager);
338 }
339
340
341 /* -- page Mon compte -- */
342 elseif($this->request->query->get('action') === 'update_username'){
343 return UserController::updateUsername($this->entityManager);
344 }
345 elseif($this->request->query->get('action') === 'update_password'){
346 return UserController::updatePassword($this->entityManager);
347 }
348
349 /* -- page Maintenance -- */
350 elseif($this->request->query->get('action') === 'restore_database' && $this->request->get('hidden') === '' && $this->request->request->has('selected_sql')){
351 return MaintenanceController::handleBackupSelection($this->entityManager, $this->request);
352 }
353
354 // redirection page d'accueil
355 return new RedirectResponse((string)new URL(['error' => 'paramètres inconnus']));
356 }
357
358 // requêtes XMLHttpRequest
359 elseif($this->request->isXmlHttpRequest()){
360 return new JsonResponse(['success' => false]); // noyer le poisson en laissant penser que le site gère les requêtes XHR
361 }
362
363 // POST admin ne matchant pas
364 return new Response('bad parameters', Response::HTTP_BAD_REQUEST); // code 400
365 }
366
367 // POST non-admin ne matchant pas
368 return new Response('bad parameters', Response::HTTP_BAD_REQUEST); // code 400
369 }
370
371 // méthode HTTP inconnue
372 else{
373 return new RedirectResponse((string)new URL(['error' => 'tu fais quoi là mec?']));
374 }
375 }
376} \ No newline at end of file
diff --git a/src/service/Security.php b/src/service/Security.php
deleted file mode 100644
index 356f4f4..0000000
--- a/src/service/Security.php
+++ /dev/null
@@ -1,110 +0,0 @@
1<?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
deleted file mode 100644
index 5bd2594..0000000
--- a/src/service/URL.php
+++ /dev/null
@@ -1,88 +0,0 @@
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/User.php b/src/service/User.php
deleted file mode 100644
index bd17dce..0000000
--- a/src/service/User.php
+++ /dev/null
@@ -1,23 +0,0 @@
1<?php
2// src/service/User.php
3
4declare(strict_types=1);
5
6use Doctrine\ORM\EntityManager;
7
8class User{
9 static public function existUsers(EntityManager $entityManager): bool
10 {
11 if(!$entityManager // table vide
12 ->createQuery("SELECT u FROM App\Entity\User u")
13 ->setMaxResults(1)
14 ->getOneOrNullResult())
15 {
16 unset($_SESSION['user']);
17 return false;
18 }
19 else{
20 return true;
21 }
22 }
23} \ No newline at end of file
diff --git a/src/service/UserDataService.php b/src/service/UserDataService.php
deleted file mode 100644
index e0156e2..0000000
--- a/src/service/UserDataService.php
+++ /dev/null
@@ -1,46 +0,0 @@
1<?php
2// src/service/UserDataService.php
3
4declare(strict_types=1);
5
6class UserDataService
7{
8 static private string $var = '../var';
9
10 static public function createZip(string $zip_name, array $source_directories, array $pattern_to_target_in_user_data = []): string
11 {
12 $file_path = self::$var . '/' . $zip_name;
13 $Zip = new ZipArchive();
14
15 if($Zip->open($file_path, ZipArchive::CREATE | ZipArchive::OVERWRITE) !== TRUE){ // ou ZipArchive::FL_OVERWRITE, à comparer à ZipArchive::OVERWRITE
16 throw new RuntimeException("Création ou ouverture du fichier demandé impossible à cause d'une erreur de permissions sur le serveur.");
17 }
18
19 // recherche récursive dans les dossiers dans $source_directories, c'est comme le paramètre "-r" dans la console
20 $counter = 0;
21 foreach($source_directories as $path){
22 $directory = new RecursiveDirectoryIterator($path);
23 $iterator = new RecursiveIteratorIterator($directory);
24
25 foreach($iterator as $info){
26 if($info->getFilename() != "." && $info->getFilename() != ".." && $info->getFilename() != '.htaccess'){ // chemins inutiles . et .. et le .htaccess
27 $Zip->addGlob($info->getPathname(), 0, array(''));
28 $counter++;
29 }
30 }
31 }
32 // recherche à la racine avec des pattern de noms de fichiers (optionnel)
33 foreach($pattern_to_target_in_user_data as $one_pattern){
34 $Zip->addGlob($one_pattern, 0, array(''));
35 }
36
37 $Zip->close();
38 if($counter > 0){
39 //chmod($file_path, 0666);
40 return $zip_name;
41 }
42 else{
43 throw new RuntimeException("Téléchargement des fichiers impossible, aucun fichier n'a été trouvé sur le serveur.");
44 }
45 }
46} \ No newline at end of file
diff --git a/src/service/session.php b/src/service/session.php
deleted file mode 100644
index 24023e9..0000000
--- a/src/service/session.php
+++ /dev/null
@@ -1,90 +0,0 @@
1<?php
2// src/service/session.php
3
4// à voir si c'est mieux avec:
5//use Symfony\Component\HttpFoundation\Session\Session;
6
7// note: session_regenerate_id(true) se trouve dans UserController::connect
8
9use Doctrine\ORM\EntityManager;
10
11function startSession(EntityManager $entityManager): void
12{
13 ini_set('session.cookie_samesite', 'Strict');
14 ini_set('session.cookie_httponly', 'On');
15 ini_set('session.use_strict_mode', 'On');
16 ini_set('session.cookie_secure', 'On');
17 session_start();
18 validateSession($entityManager);
19}
20
21function validateSession(EntityManager $entityManager): void
22{
23 if(defined('IS_ADMIN')){
24 return;
25 }
26
27 $is_admin = false;
28
29 if(isset($_SESSION['user']['id'])){
30 $user = UserController::getUserById($_SESSION['user']['id'], $entityManager);
31
32 // visiteur normal
33 if(!$user){
34 session_unset();
35 session_destroy();
36 header('Location: ' . new URL(['message' => 'session_invalide']));
37 die;
38 }
39
40 // MAJ de la session avec CERTAINES données
41 $_SESSION['user']['username'] = $user->getLogin();
42 $_SESSION['user']['role'] = $user->getRole();
43
44 $is_admin = $user->getRole() === 'admin';
45 }
46
47 define('IS_ADMIN', $is_admin);
48
49 // si on a un jour besoin d'une variable globale au lieu d'une constante
50 //$GLOBALS['is_admin'] = $is_admin; // version modifiable 1
51 /*function isAdmin(): bool { // version modifiable 2
52 return $_SESSION['user']['role'] ?? null === 'admin';
53 }*/
54
55
56 // => système de cache à ajouter pour ne pas lire la BDD à chaque fois
57 //remplacer ce qui est en haut
58 /*$user = $_SESSION['user'] ?? null;
59 if (!$user) {
60 // visiteur
61 }
62 // Vérification périodique (ex: toutes les 5 minutes)
63 if (time() - $user['last_check'] > 300) {
64 $user = UserController::getUserById($user['id'], $entityManager);
65 if (!$user) {
66 session_destroy();
67 header('Location: /login.php');
68 exit;
69 }
70 // cache pour ne pas avoir à lire la BDD à chaque page
71 $_SESSION['user'] = [
72 'id' => $user['id'],
73 'role' => $user['role'],
74 'username' => $user['username'],
75 'last_check' => time()
76 ];
77 $user = $_SESSION['user'];
78 }
79 $is_admin = ($user['role'] === 'admin');*/
80
81
82 // améliorations possibles: ajouter expiration automatique + protection contre vol de session (IP / user-agent) sans casser ton app.
83}
84
85// nettoyage complet
86/*function cleanSession(){
87 unset($_SESSION['user']); // mémoire vive
88 session_destroy(); // fichier côté serveur
89 setcookie('PHPSESSID', '', time() - 86400, '/'); // cookie de session
90}*/ \ No newline at end of file