From f27cca08ff786ceff1870147f6d6bad14d71117f Mon Sep 17 00:00:00 2001
From: polo
Date: Fri, 23 Oct 2026 00:47:32 +0200
Subject: =?UTF-8?q?dossiers=20avec=20majuscule,=20namespace=20pour=20toute?=
=?UTF-8?q?s=20les=20classes,=20autoload=20psr-4,=20UserDataService::creat?=
=?UTF-8?q?eZip=20d=C3=A9plac=C3=A9=20dans=20Backup?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/Service/Backup.php | 272 ++++++++++++++++++++++++++++
src/Service/Captcha.php | 57 ++++++
src/Service/Config.php | 90 ++++++++++
src/Service/EmailService.php | 106 +++++++++++
src/Service/FormValidation.php | 216 ++++++++++++++++++++++
src/Service/Installation.php | 109 ++++++++++++
src/Service/Router.php | 393 +++++++++++++++++++++++++++++++++++++++++
src/Service/Security.php | 112 ++++++++++++
src/Service/URL.php | 90 ++++++++++
src/Service/User.php | 25 +++
src/Service/session.php | 94 ++++++++++
11 files changed, 1564 insertions(+)
create mode 100644 src/Service/Backup.php
create mode 100644 src/Service/Captcha.php
create mode 100644 src/Service/Config.php
create mode 100644 src/Service/EmailService.php
create mode 100644 src/Service/FormValidation.php
create mode 100644 src/Service/Installation.php
create mode 100644 src/Service/Router.php
create mode 100644 src/Service/Security.php
create mode 100644 src/Service/URL.php
create mode 100644 src/Service/User.php
create mode 100644 src/Service/session.php
(limited to 'src/Service')
diff --git a/src/Service/Backup.php b/src/Service/Backup.php
new file mode 100644
index 0000000..546e81e
--- /dev/null
+++ b/src/Service/Backup.php
@@ -0,0 +1,272 @@
+format('Y-m-d') . '_' . $type . '.sql';
+
+ // les versions de mysql sont comme ci: 8.0.36
+ // celles de mariadb sont comme ça: 10.11.6-MariaDB
+ $version = $entityManager->getConnection()->fetchOne('SELECT VERSION()');
+ $engine = stripos($version, 'mariadb') !== false ? 'mariadb-dump' : 'mysqldump';
+
+ $tmp = tempnam('../var', 'tmp_db_codes_'); // crée un fichier avec un nom aléatoire et des droits 600 (concurrence)
+ // attention, si le dossier de destination n'est pas disponible, le fichier est placé avec les fichiers temporaires
+ file_put_contents($tmp,
+ "[client]\n
+ user=" . Config::$user . "\n
+ password=" . Config::$password . "\n
+ host=" . Config::$db_host . "\n");
+
+ $command = new Process([
+ $engine,
+ '--defaults-extra-file=' . $tmp, // pour ne pas enregistrer les codes dans l'historique de la console ou dans les processus de l'OS
+ '--single-transaction',
+ '--quick', // évite d'exploser la RAM si beaucoup de données
+ '--result-file=' . $file_path,
+ Config::$database
+ ]);
+
+ try{
+ // unlink et chmod permettent que le serveur et l'utilisateur appelant bin/mysqldump.php réussissent
+ if(file_exists($file_path)){
+ unlink($file_path);
+ }
+ $command->mustRun(); // comme run() mais lance une ProcessFailedException
+
+ //$file_path = self::gzipCompress($file_path); // '.gz' ajouté à la fin
+
+ chmod($file_path, 0666);
+ return $file_path;
+ }
+ finally{
+ // exécuté même quand situé après "return"
+ unlink($tmp);
+ self::cleanBackups();
+ }
+ }
+
+ // compression gzip (gros gain de place sur le serveur), nécessite l'extension zlib
+ static public function gzipCompress(string $file_path): string
+ {
+ try{
+ file_put_contents(
+ $file_path . '.gz',
+ gzencode(file_get_contents($file_path), 5), // plus rapide que 9 et taille identique d'après mes essais
+ );
+ unlink($file_path);
+ $file_path .= '.gz';
+ }
+ finally{
+ return $file_path;
+ }
+ }
+
+ static public function getBackupList(): array
+ {
+ $files = scandir(Backup::$backup_dir); // affiche un warning si échoue (à cacher en prod)
+ if(!$files){
+ throw new \RuntimeException("Le serveur a rencontré une erreur:
Accès aux backups impossible faute de permissions.");
+ }
+
+ $backup_array = [];
+ foreach($files as $file){
+ if($file[0] === '.'){
+ continue;
+ }
+ $backup_array[] = $file;
+ }
+ return $backup_array;
+ }
+
+ static public function cleanBackups(): void
+ {
+ $files = glob(self::$backup_dir . '/*.sql');
+ //usort($files, fn($a, $b) => filemtime($b) <=> filemtime($a)); // filemtime = date de dernière modification
+ arsort($files);
+
+ // tri par nom de BDD puis par date
+ $sorted_files = [];
+ $list_by_database = []; // pour le nettoyage 2
+ foreach($files as $file){
+ $exploded = explode('_', basename($file));
+ $sorted_files[$exploded[0]][$exploded[1]][] = $file;
+ $list_by_database[$exploded[0]][] = $file;
+ }
+
+ $date = new \DateTime;
+ foreach($sorted_files as $db_name => $from_one_database){
+ // on garde une "quantité à garder" par BDD
+ if(count($from_one_database) > self::$amount_to_keep){
+ // nettoyage 1
+ foreach($from_one_database as $date_key => $with_same_date){
+ // pas touche à aujourd'hui
+ if($date_key != $date->format('Y-m-d')){
+ self::cleanBackupsByPriority($with_same_date);
+ }
+ }
+ // nettoyage 2
+ $files_to_delete = array_slice($list_by_database[$db_name], self::$amount_to_keep);
+ foreach($files_to_delete as $file){
+ unlink($file);
+ }
+ }
+ }
+ }
+
+ // conserver un seul backup par jour choisi dans cet ordre de préférence: console => before-restore => download => auto
+ // cet ordre correspond à: volonté de l'utilisateur => état du jour avant changement => volonté de changement => automatique sans contrôle
+ static private function cleanBackupsByPriority(array $files): void
+ {
+ $priorities = [
+ 'console' => 1,
+ 'before-restore' => 2,
+ 'uploaded' => 3,
+ 'auto' => 4,
+ ];
+ $best_priority = PHP_INT_MAX;
+
+ // recherche du fichier à conserver
+ $to_keep = null;
+ foreach($files as $file){
+ foreach($priorities as $keyword => $priority){
+ if(str_contains(basename($file), $keyword) && $priority < $best_priority){
+ $best_priority = $priority;
+ $to_keep = $file;
+ break;
+ }
+ }
+ }
+ // suppression des autres
+ foreach($files as $file){
+ if($file !== $to_keep){
+ unlink($file);
+ }
+ }
+ }
+
+ static public function restoreDatabase(EntityManager $entityManager, string $file_name, array $exclusion_list = []): void
+ {
+ // création d'un backup de sécurité non écrasable
+ $date = new \DateTime;
+ if(!file_exists(self::$backup_dir . '/' . Config::$database . '_' . $date->format('Y-m-d') . '_before-restore.sql')){
+ Backup::mySQLdump($entityManager, 'before-restore');
+ }
+
+ $version = $entityManager->getConnection()->fetchOne('SELECT VERSION()');
+ $engine = stripos($version, 'mariadb') !== false ? 'mariadb' : 'mysql';
+
+ $tables = self::getDBListOfTables($entityManager, $exclusion_list); // choisir les tables à effacer et restaurer, exclure "user"
+
+ $tmp_db_codes = tempnam('../var', 'tmp_db_codes_'); // crée un fichier avec un nom aléatoire et des droits 600 (concurrence)
+ file_put_contents($tmp_db_codes,
+ "[client]\n
+ user=" . Config::$user . "\n
+ password=" . Config::$password . "\n
+ host=" . Config::$db_host . "\n");
+
+ //$file_name = self::gzipExtract($file_name); // '.gz' ajouté à la fin
+
+ $command = new Process([
+ $engine, // mariadb ou mysql
+ '--defaults-extra-file=' . $tmp_db_codes, // pour ne pas enregistrer les codes dans l'historique de la console ou dans les processus de l'OS
+ Config::$database
+ ]);
+ $command->setInput(file_get_contents(Backup::$backup_dir . '/' . $file_name)); // l'entrée <
+
+ // utiliser une liste de tables à exclure dynamique
+ try{
+ // tout effacer
+ $tables_with_backquotes = array_map(fn($t) => '`' . $t . '`', $tables);
+ if(!empty($tables_with_backquotes)){
+ $sql = "SET FOREIGN_KEY_CHECKS=0; DROP TABLE " . implode(', ', $tables_with_backquotes) . "; SET FOREIGN_KEY_CHECKS=1;";
+ $entityManager->getConnection()->executeStatement($sql);
+ }
+
+ // copie des tables à ne pas restaurer
+ foreach($exclusion_list as $excluded){
+ $entityManager->getConnection()->executeStatement('RENAME TABLE `' . TABLE_PREFIX . $excluded . '` TO `' . TABLE_PREFIX . $excluded . '_dont_touch`;');
+ }
+
+ // restaurer
+ $command->mustRun(); // comme run() mais lance une ProcessFailedException
+
+ // remettre table user comme avant
+ foreach($exclusion_list as $excluded){
+ $entityManager->getConnection()->executeStatement('DROP TABLE `' . TABLE_PREFIX . $excluded . '`;');
+ $entityManager->getConnection()->executeStatement('RENAME TABLE `' . TABLE_PREFIX . $excluded . '_dont_touch` TO `' . TABLE_PREFIX . $excluded . '`;');
+ }
+ }
+ finally{
+ // exécuté même quand situé après "return"
+ unlink($tmp_db_codes);
+ }
+ }
+
+ static private function getDBListOfTables(EntityManager $entityManager, array $exclusion_list = []): array
+ {
+ $tables = $entityManager->getConnection()->createSchemaManager()->listTableNames();
+ foreach($tables as $key => $elem){
+ foreach($exclusion_list as $excluded){
+ if(!is_string($excluded)){ // cas pas du tout censé arriver!
+ throw new \LogicException("Un nom de table doit etre une chaîne de caractères.");
+ }
+ if($elem === TABLE_PREFIX . $excluded){
+ unset($tables[$key]);
+ break; // petite optimisation
+ }
+ }
+ }
+ return $tables;
+ }
+
+ static public function createZip(string $zip_name, array $source_directories, array $pattern_to_target_in_user_data = []): string
+ {
+ $file_path = '../var/' . $zip_name;
+ $Zip = new \ZipArchive();
+
+ if($Zip->open($file_path, \ZipArchive::CREATE | \ZipArchive::OVERWRITE) !== TRUE){ // ou ZipArchive::FL_OVERWRITE, à comparer à ZipArchive::OVERWRITE
+ throw new \RuntimeException("Création ou ouverture du fichier demandé impossible à cause d'une erreur de permissions sur le serveur.");
+ }
+
+ // recherche récursive dans les dossiers dans $source_directories, c'est comme le paramètre "-r" dans la console
+ $counter = 0;
+ foreach($source_directories as $path){
+ $directory = new \RecursiveDirectoryIterator($path);
+ $iterator = new \RecursiveIteratorIterator($directory);
+
+ foreach($iterator as $info){
+ if($info->getFilename() != "." && $info->getFilename() != ".." && $info->getFilename() != '.htaccess'){ // chemins inutiles . et .. et le .htaccess
+ $Zip->addGlob($info->getPathname(), 0, array(''));
+ $counter++;
+ }
+ }
+ }
+ // recherche à la racine avec des pattern de noms de fichiers (optionnel)
+ foreach($pattern_to_target_in_user_data as $one_pattern){
+ $Zip->addGlob($one_pattern, 0, array(''));
+ }
+
+ $Zip->close();
+ if($counter > 0){
+ //chmod($file_path, 0666);
+ return $zip_name;
+ }
+ else{
+ throw new \RuntimeException("Téléchargement des fichiers impossible, aucun fichier n'a été trouvé sur le serveur.");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Service/Captcha.php b/src/Service/Captcha.php
new file mode 100644
index 0000000..28c7f71
--- /dev/null
+++ b/src/Service/Captcha.php
@@ -0,0 +1,57 @@
+ on pourrait appliquer le pattern "singleton" (justification = le captcha devient une sorte de ressource partagée)
+
+declare(strict_types=1);
+
+namespace App\Service;
+
+class Captcha
+{
+ private int $a;
+ private int $b;
+
+ public function __construct(){
+ $this->a = rand(2, 9);
+ $this->b = rand(2, 9);
+ }
+
+ public function getA(): string
+ {
+ return $this->toLettersFrench($this->a);
+ }
+ public function getB(): string
+ {
+ return $this->toLettersFrench($this->b);
+ }
+ public function getSolution(): int
+ {
+ return ($this->a * $this->b);
+ }
+
+ private function toLettersFrench(int $number): string
+ {
+ return match($number){
+ 2 => 'deux',
+ 3 => 'trois',
+ 4 => 'quatre',
+ 5 => 'cinq',
+ 6 => 'six',
+ 7 => 'sept',
+ 8 => 'huit',
+ 9 => 'neuf',
+ default => '', // erreur
+ };
+ }
+
+ // (à déplacer dans FormValidation?)
+ static public function controlInput(string $input = '0'): int
+ {
+ // un POST est une chaîne qu'on doit convertir en nombre dans deux conditions:
+ // test de format: $input est un nombre
+ // test d'intégrité: supprimer les décimales avec (int) ne change pas la valeur du nombre
+ return is_numeric($input) && $input == (int)$input ? (int)$input : 0;
+ }
+}
\ No newline at end of file
diff --git a/src/Service/Config.php b/src/Service/Config.php
new file mode 100644
index 0000000..fc4d127
--- /dev/null
+++ b/src/Service/Config.php
@@ -0,0 +1,90 @@
+Le fichier config/config.ini n'existe pas ou n'est pas lisible.
";
+ }
+ define('TABLE_PREFIX', self::$table_prefix);
+ }
+
+ // renseigner les variables internes de Config
+ static private function hydrate(array $raw_data): void
+ {
+ foreach($raw_data as $field => $value)
+ {
+ if($value != '') // valeur par défaut
+ {
+ if(isset(self::$$field)) // le champ existe dans Config
+ {
+ // problème du slash à la fin du nom d'un dossier
+ $value = self::slashAtEndOfPath($field, $value);
+ self::$$field = $value;
+ }
+ else
+ {
+ echo "debug: le fichier config.ini comporte une erreur, le champ: " . $field . " est incorrect,\nl'information contenue sur cette ligne ne sera pas utilisée\n";
+ }
+ }
+ /*else
+ {
+ echo "debug: le champ " . $field . " est vide, la valeur par défaut " . self::$$field . " sera utilisée.\n";
+ }*/
+ }
+ }
+
+
+ // pour que les chemins finissent toujours par un /
+ static private function slashAtEndOfPath(string $field, string $value): string
+ {
+ foreach(self::$path_vars as $item)
+ {
+ if($field === $item){
+ return !str_ends_with($value, '/') ? $value . '/' : $value;
+ }
+ }
+ return $value;
+ }
+}
diff --git a/src/Service/EmailService.php b/src/Service/EmailService.php
new file mode 100644
index 0000000..38b4cf8
--- /dev/null
+++ b/src/Service/EmailService.php
@@ -0,0 +1,106 @@
+ exceptions
+ $mail->CharSet = 'UTF-8';
+
+ $smtp_host = $form_data->getData()['smtp_host'] ?? Config::$smtp_host;
+ $smtp_secure = $form_data->getData()['smtp_secure'] ?? Config::$smtp_secure;
+ $smtp_username = $form_data->getData()['smtp_username'] ?? Config::$smtp_username;
+ $smtp_password = $form_data->getData()['smtp_password'] ?? Config::$smtp_password;
+
+ $email_from = $form_data->getData()['email_from'] ?? Config::$email_from; // adresse de l'expéditeur affichée
+ $email_from = empty($email_from) ? $smtp_username : $email_from; // si vide
+ $email_from_name = $form_data->getData()['email_from_name'] ?? Config::$email_from_name; // = nom de l'expéditeur affiché
+ $email_dest = $form_data->getData()['email_dest'] ?? Config::$email_dest;
+ $email_dest_name = $form_data->getData()['email_dest_name'] ?? Config::$email_dest_name; // = destinataire formulaire
+
+ try{
+ // Paramètres du serveur
+ $mail->isSMTP();
+ $mail->Host = $smtp_host;
+ $mail->SMTPAuth = true;
+ $mail->Port = 25;
+
+ if($mail->SMTPAuth){
+ $mail->Username = $smtp_username; // e-mail
+ $mail->Password = $smtp_password;
+ $mail->SMTPSecure = $smtp_secure; // tls (starttls) ou ssl (smtps)
+ if($mail->SMTPSecure === 'tls'){
+ $mail->Port = 587;
+ }
+ elseif($mail->SMTPSecure === 'ssl'){
+ $mail->Port = 465;
+ }
+ }
+ //var_dump($mail->smtpConnect());die; // test de connexion
+
+ // Expéditeur et destinataire
+ // $email_from, $email_from_name et $email_dest_name sont modifiables uniquement dans le config.ini pour l'instant
+ $mail->setFrom(strtolower($email_from), $email_from_name);
+ $mail->addAddress(strtolower($email_dest), $email_dest_name);
+
+ // Contenu
+ $mail->isHTML(true);
+ if($test_email){
+ $mail->Subject = "TEST d'un envoi d'e-mail depuis le site web";
+ }
+ else{
+ $mail->Subject = 'Message envoyé par: ' . $name . ' (' . $email . ') depuis le site web';
+ }
+ $mail->Body = $message;
+ $mail->AltBody = $message;
+
+ $mail->send();
+
+ // copie en BDD
+ if(!$test_email && ($form_data->getData()['keep_emails'] ?? self::KEEP_EMAILS_DEFAULT)){
+ $db_email = new Email($name, $email, $email_dest, $message, $form_data);
+ $entityManager->persist($db_email);
+ self::updateLastContactDate($entityManager, $email);
+ $entityManager->flush();
+ }
+
+ return true;
+ }
+ catch(\Exception $e){
+ echo "Le message n'a pas pu être envoyé. Erreur : {$e}
{$mail->ErrorInfo}";
+ return false;
+ }
+ }
+
+ static public function updateLastContactDate(EntityManager $entityManager, string $sender): void
+ {
+ foreach($entityManager->getRepository('App\Entity\Email')->findAll() as $email){
+ $email->getSenderAddress() === $sender ? $email->updateLastContactDate() : null;
+ }
+ }
+
+ // peut être appelée par bin/clean_emails_cron.php
+ static public function cleanEmails(EntityManager $entityManager): void
+ {
+ $emails = $entityManager->getRepository('App\Entity\Email')->findAll();
+ foreach($emails as $email){
+ if($email->getDeletionDate() < new \DateTime()){
+ $entityManager->remove($email);
+ }
+ }
+ $entityManager->flush();
+ }
+}
\ No newline at end of file
diff --git a/src/Service/FormValidation.php b/src/Service/FormValidation.php
new file mode 100644
index 0000000..d91aa12
--- /dev/null
+++ b/src/Service/FormValidation.php
@@ -0,0 +1,216 @@
+ 'emailSendStrategy',
+ self::EmailParam => 'emailParamStrategy',
+ self::CreateUser => 'createUserStrategy',
+ self::Connection => 'connectionStrategy',
+ self::UsernameUpdate => 'usernameUpdateStrategy',
+ self::PasswordUpdate => 'passwordUpdateStrategy'
+ };
+ }
+}
+
+class FormValidation
+{
+ private array $data; // tableau associatif (probablement $_POST)
+ private \Closure $callStrategy;
+ private ?string $error;
+
+ public function __construct(array $data, ValidationStrategy $strategy){
+ $this->data = $data;
+ $method = $strategy->method();
+ $this->callStrategy = fn() => $this->$method();
+ }
+
+ public function validate(): bool
+ {
+ $this->error = ($this->callStrategy)(); // appel de la callback
+ return empty($this->errors);
+ }
+
+ public function getError(): string
+ {
+ return $this->error;
+ }
+
+ public function getField(string $field): string
+ {
+ return $this->data[$field] ?? '';
+ }
+
+ // méthodes de validation
+ private function captchaValidate(bool $clean_session = true): ?string
+ {
+ $captcha_solution = (isset($_SESSION['captcha']) && is_int($_SESSION['captcha'])) ? $_SESSION['captcha'] : 0;
+ $captcha_try = isset($this->data['captcha']) ? Captcha::controlInput($this->data['captcha']) : 0;
+ if($clean_session){
+ unset($_SESSION['captcha']);
+ }
+
+ if($captcha_try == 0){
+ return 'error_non_valid_captcha';
+ }
+ elseif($captcha_solution == 0){ // ne peut pas arriver, si?
+ return 'captcha_server_error';
+ }
+ elseif($captcha_try !== $captcha_solution){
+ return 'bad_solution_captcha';
+ }
+ return null;
+ }
+
+ // erreurs à la création des mots de passe
+ static private function removeSpacesTabsCRLF(string $chaine): string
+ {
+ $cibles = [' ', "\t", "\n", "\r"]; // doubles quotes !!
+ return(str_replace($cibles, '', $chaine));
+ }
+
+
+ // stratégies
+ private function emailSendStrategy(): ?string
+ {
+ if($error = $this->captchaValidate(false)){
+ return $error;
+ }
+
+ if(empty($this->data['name']) || empty($this->data['email']) || empty($this->data['message'])
+ || !isset($this->data['hidden']) || !empty($this->data['hidden'])){
+ return 'missing_fields';
+ }
+
+ elseif(!filter_var(trim($this->data['email']), FILTER_VALIDATE_EMAIL)){
+ return 'bad_email_address';
+ }
+
+ $this->data['name'] = htmlspecialchars(trim($this->data['name']));
+ $this->data['email'] = htmlspecialchars(trim($this->data['email']));
+ $this->data['message'] = htmlspecialchars($this->data['message']);
+
+ return null;
+ }
+ private function emailParamStrategy(): ?string
+ {
+ if(empty($this->data['id']) || empty($this->data['what_param']) || empty($this->data['value'])
+ || !isset($this->data['hidden']) || !empty($this->data['hidden'])){
+ return 'missing_fields';
+ }
+
+ if(!in_array($this->data['what_param'], ['smtp_host', 'smtp_secure', 'smtp_username', 'smtp_password', 'email_dest'])){
+ return 'unknown_parameter';
+ }
+ elseif($this->data['what_param'] === 'smtp_username' || $this->data['what_param'] === 'email_dest'){
+ if(!filter_var($this->data['value'], FILTER_VALIDATE_EMAIL)){
+ return 'invalide_email_address';
+ }
+ }
+ return null;
+ }
+ private function createUserStrategy(): ?string
+ {
+ if($error = $this->captchaValidate()){
+ return $error;
+ }
+
+ // test mauvais paramètres
+ if(empty($this->data['login']) || empty($this->data['password']) || empty($this->data['password_confirmation'])
+ || !isset($this->data['create_user_hidden']) || !empty($this->data['create_user_hidden']))
+ {
+ return 'bad_login_or_password';
+ }
+
+ if($this->data['password'] !== $this->data['password_confirmation']){
+ return 'different_passwords';
+ }
+
+ if($this->data['login'] !== self::removeSpacesTabsCRLF(htmlspecialchars($this->data['login']))
+ || $this->data['password'] !== self::removeSpacesTabsCRLF(htmlspecialchars($this->data['password']))){
+ return 'forbidden_characters';
+ }
+ return null;
+ }
+ private function connectionStrategy(): ?string
+ {
+ if($error = $this->captchaValidate()){
+ return $error;
+ }
+
+ if(empty($this->data['login']) || empty($this->data['password'])
+ || !isset($this->data['connection_hidden']) || !empty($this->data['connection_hidden']))
+ {
+ return 'bad_login_or_password';
+ }
+ return null;
+ }
+ private function usernameUpdateStrategy(): ?string
+ {
+ if($error = $this->captchaValidate()){
+ return $error;
+ }
+
+ if(empty($this->data['login']) || empty($this->data['password']) || empty($this->data['new_login'])
+ || !isset($this->data['modify_username_hidden']) || !empty($this->data['modify_username_hidden']))
+ {
+ return 'bad_login_or_password';
+ }
+
+ $new_login = self::removeSpacesTabsCRLF(htmlspecialchars($this->data['new_login']));
+ if($new_login !== $this->data['new_login']){
+ return 'forbidden_characters';
+ }
+
+ if($this->data['login'] !== $_SESSION['user']['username']){
+ return 'bad_login_or_password';
+ }
+ if($this->data['login'] === $new_login){
+ return 'same_username_as_before';
+ }
+ return null;
+ }
+ private function passwordUpdateStrategy(): ?string
+ {
+ if($error = $this->captchaValidate()){
+ return $error;
+ }
+
+ if(empty($this->data['login']) || empty($this->data['password']) || empty($this->data['new_password'])
+ || !isset($this->data['modify_password_hidden']) || !empty($this->data['modify_password_hidden']))
+ {
+ return 'bad_login_or_password';
+ }
+
+ $new_password = self::removeSpacesTabsCRLF(htmlspecialchars($this->data['new_password']));
+ if($new_password !== $this->data['new_password']){
+ return 'forbidden_characters';
+ }
+
+ if($this->data['login'] !== $_SESSION['user']['username']){
+ return 'bad_login_or_password';
+ }
+ if($this->data['password'] === $new_password){
+ return 'same_password_as_before';
+ }
+ return null;
+ }
+}
\ No newline at end of file
diff --git a/src/Service/Installation.php b/src/Service/Installation.php
new file mode 100644
index 0000000..ed9f20a
--- /dev/null
+++ b/src/Service/Installation.php
@@ -0,0 +1,109 @@
+L'extension " . $extension . " est manquante.");
+ $flag = true;
+ }
+ }
+ if(!class_exists(\DOMDocument::class)){ // théoriquement plus fiable que extension_loaded()
+ echo("L'extension dom est manquante.
");
+ $flag = true;
+ }
+
+ /*if(!extension_loaded('imagick') && !extension_loaded('gd')){
+ echo("il manque une de ces extensions au choix pour le traitement des images: imagick (de préférence) ou gd.
");
+ $flag = true;
+ }*/
+ // si imagick n'est pas disponible, essayer gd (reste encore à coder)
+
+ if($flag){
+ echo 'Réalisez les actions nécéssaires sur le serveur ou contactez l\'administrateur du site.
+ Quand le problème sera résolu, il vous suffira de recharger la page.
';
+ die;
+ }
+ }
+
+ static public function checkFilesAndFoldersRights(): void
+ {
+ // -- droits des fichiers et dossiers --
+ $droits_dossiers = 0777;
+
+ $flag = false;
+ if(!file_exists('user_data')){
+ try{
+ mkdir('user_data/');
+ chmod('user_data/', $droits_dossiers);
+ }
+ catch(\Exception $e){
+ echo 'Le dossier public/user_data introuvable et le serveur n\'a pas la permission de le créer.
+ Pour faire ça bien:
sudo -u "serveur web" mkdir /chemin/du/site/public/user_data
';
+ echo $e;
+ $flag = true;
+ }
+ }
+ if(!file_exists('../var')){
+ try{
+ mkdir('../var');
+ chmod('../var', $droits_dossiers);
+ }
+ catch(\Exception $e){
+ echo $e;
+ $flag = true;
+ }
+ }
+ if(!file_exists('../var/backups')){
+ try{
+ mkdir('../var/backups');
+ chmod('../var/backups', $droits_dossiers); // autoriser à la fois le serveur et les scripts dans bin/
+ }
+ catch(\Exception $e){
+ echo $e;
+ $flag = true;
+ }
+ }
+
+ // droits 600 pour celui-ci
+ if(!file_exists('../config/config.ini')){
+ // aide à la création du config.ini
+ echo 'Le fichier config/config.ini est introuvable.
';
+ echo 'Il doit obligatoirement contenir les codes de la base de données, le protocole http ou https (et éventuellement le port) utilisé pour créer les liens internes.
+ Un modèle est disponible, il s\'agit du fichier config/config-template.ini
+ Ce fichier a une importance critique. Si vous le pouvez faites en sorte que le serveur en soit le propriétaire et donner lui des droits 600.
';
+ $flag = true;
+ }
+ /*else{
+ // propriétaire du fichier
+ if(fileowner('../config/config.ini') != posix_geteuid()){
+ echo "le fichier config/config.ini n'appartient pas au serveur.
";
+ }
+ else{
+ // droits du config.ini
+ if(substr(sprintf('%o', fileperms('../config/config.ini')), -4) != 600){
+ echo 'Attention, le
';
+ //chmod('../config/config.ini', $droits_fichiers);
+ }
+ }
+ }*/
+ if($flag){
+ die;
+ }
+
+ // tester les liens internes
+ //
+
+ // le test de connexion à la BDD est dans le doctrine bootstrap
+ }
+}
\ No newline at end of file
diff --git a/src/Service/Router.php b/src/Service/Router.php
new file mode 100644
index 0000000..98cecc5
--- /dev/null
+++ b/src/Service/Router.php
@@ -0,0 +1,393 @@
+ 1er test, méthode http GET? POST?
+=> 2ème test, type de contenu:
+"application/x-www-form-urlencoded" = formulaire
+"application/json" = requête AJAX avec fetch()
+"multipart/form-data" = upload d'image par tinymce
+$_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest' requête AJAX xhs, non utilisée
+=> 3ème test, comme le 2ème test mais uniquement si IS_ADMIN est vrai */
+
+/* classes de réponses pour les contrôleurs
+Response($html, Response::HTTP_OK) page html
+JsonResponse(['success' => true, 'data' => $data]) ajax
+RedirectResponse('index.php?page=login') redirection
+BinaryFileResponse($filePath) téléchargement
+StreamedResponse(function () {echo "ligne 1\n";echo "ligne 2\n";}) gros fichier */
+
+// relire ça à l'occaz:
+// https://symfony.com/doc/current/introduction/from_flat_php_to_symfony.html
+
+declare(strict_types=1);
+
+namespace App\Service;
+
+use Symfony\Component\HttpFoundation\Request;
+use Doctrine\ORM\EntityManager;
+use Symfony\Component\HttpFoundation\JsonResponse;
+use Symfony\Component\HttpFoundation\RedirectResponse;
+use Symfony\Component\HttpFoundation\Response;
+use App\Service\User;
+use App\View\AbstractBuilder;
+use App\Model\Model;
+use App\Model\Menu;
+use App\Controller\UserController;
+use App\Controller\ArticleController;
+use App\Controller\CalendarController;
+use App\Controller\MaintenanceController;
+use App\Controller\ViewController;
+use App\Controller\ContactFormController;
+use App\Controller\ImageUploadController;
+use App\Controller\HeadFootController;
+use App\Controller\MenuAndPathsController;
+use App\Controller\PageManagementController;
+use App\Controller\FileUploadController;
+
+class Router{
+ private Request $request;
+ private EntityManager $entityManager;
+ private string $route = ''; // défaut page d'accueil
+
+ public function __construct(Request $request, EntityManager $entityManager){
+ $this->request = $request;
+ $this->entityManager = $entityManager;
+
+ if(!User::existUsers($entityManager)){ // table "user" vide
+ $this->route = 'no_user';
+ }
+ }
+
+ public function dispatch(): Response
+ {
+ if($this->request->getMethod() === 'GET'){
+ // table "user" vide
+ if($this->route === 'no_user'){
+ ob_start();
+ require AbstractBuilder::VIEWS_PATH . 'user_create.php';
+ return new Response(ob_get_clean());
+ }
+
+ // bouton déconnexion (méthode GET parce que l'utilisateur ne modifie plus de données à partir de là)
+ if($this->request->query->get('action') === 'deconnection'){
+ return UserController::disconnect(); // retourne un RedirectResponse
+ }
+
+ // articles suivants
+ if($this->request->query->get('fetch') === 'next_articles'){
+ return ArticleController::fetch($this->entityManager, $this->request); // retourne un JsonResponse
+ }
+
+ // données du calendrier
+ // création du calendrier ou changement de dates affichées (boutons flèches mais pas changement de vue)
+ if($this->request->query->get('action') === 'get_events'
+ && $this->request->query->has('start') && $this->request->query->has('end') && empty($this->request->getPayload()->all())){ // getPayload ne récupère pas que des POST
+ return CalendarController::getData($this->entityManager);
+ }
+
+ // pages interdites
+ if(!IS_ADMIN && in_array(CURRENT_PAGE, ['menu_paths', 'new_page', 'user_edit', 'emails', 'maintenance'])){
+ return new RedirectResponse((string)new URL);
+ }
+
+ if(IS_ADMIN){
+ if($this->request->query->get('action') === 'get_mysqldump'){
+ return MaintenanceController::getLastDump($this->entityManager);
+ }
+ if($this->request->query->get('action') === 'get_all_media'){
+ return MaintenanceController::getAllMedia();
+ }
+ }
+
+ // affichage d'une page
+ return ViewController::getWebPage($this->entityManager, $this->request);
+ }
+
+
+ elseif($this->request->getMethod() === 'POST'){
+ /* -- contrôleurs appelables par tout le monde -- */
+
+ // table "user" vide
+ if($this->route === 'no_user'){
+ return UserController::createAdminUser($this->entityManager);
+ }
+
+ // requête JSON avec fetch()
+ if($this->request->headers->get('Content-Type') === 'application/json')
+ {
+ $json = json_decode($this->request->getContent(), true); // = json_decode(file_get_contents('php://input'), true);
+
+ // formulaire de contact
+ if($this->request->query->get('action') === 'send_email'){
+ return ContactFormController::sendVisitorEmail($this->entityManager, $json);
+ }
+ /*else{
+ return new JsonResponse(['success' => false, 'error' => 'tu fais quoi là mec?'], JsonResponse::HTTP_BAD_REQUEST);
+ }*/
+ }
+
+ // envoi formulaire HTML
+ elseif($this->request->headers->get('Content-Type') === 'application/x-www-form-urlencoded'){
+ // tentative de connexion
+ if($this->request->query->get('action') === 'connection'){
+ return UserController::connect($this->entityManager);
+ }
+ /*else{
+ return new RedirectResponse((string)new URL(['error' => 'tu fais quoi là mec?']));
+ }*/
+ }
+
+ if(IS_ADMIN){
+ /* -- requêtes AJAX -- */
+
+ // requêtes JSON avec fetch()
+ if($this->request->headers->get('Content-Type') === 'application/json'){
+ $json = json_decode($this->request->getContent(), true); // = json_decode(file_get_contents('php://input'), true);
+
+ if($this->request->query->has('action')){
+ /* -- manipulation des articles -- */
+ if($this->request->query->get('action') === 'editor_submit' && isset($json['id']) && isset($json['content'])){
+ return ArticleController::editorSubmit($this->entityManager, $json);
+ }
+ elseif($this->request->query->get('action') === 'delete_article' && isset($json['id'])){
+ return ArticleController::deleteArticle($this->entityManager, $this->request); // version AJAX
+ }
+ elseif($this->request->query->get('action') === 'switch_positions' && isset($json['id1']) && isset($json['id2'])){
+ return ArticleController::switchPositions($this->entityManager, $json);
+ }
+ elseif($this->request->query->get('action') === 'date_submit' && isset($json['id']) && isset($json['date'])){
+ return ArticleController::dateSubmit($this->entityManager, $json);
+ }
+
+ switch($this->request->query->get('action')){
+ /* -- bloc Formulaire -- */
+ case 'keep_emails':
+ return ContactFormController::keepEmails($this->entityManager, $json);
+ case 'set_retention_period':
+ return ContactFormController::setEmailsRetentionPeriod($this->entityManager, $json);
+ case 'set_email_param':
+ return ContactFormController::setEmailParam($this->entityManager, $json);
+ case 'test_email':
+ return ContactFormController::sendTestEmail($this->entityManager, $json);
+
+ /* -- page emails -- */
+ case 'delete_email':
+ return ContactFormController::deleteEmail($this->entityManager, $json);
+ case 'toggle_sensitive_email':
+ return ContactFormController::toggleSensitiveEmail($this->entityManager, $json);
+
+ /* -- upload d'image dans tinymce par copier-coller -- */
+ // collage de HTML contenant une ou plusieurs balises
+ case 'upload_image_url':
+ return ImageUploadController::uploadImageHtml();
+ // collage d'une image (code base64 dans le presse-papier) non encapsulée dans du HTML
+ case 'upload_image_base64':
+ return ImageUploadController::uploadImageBase64();
+
+ /* -- requêtes spécifiques au calendrier -- */
+ case 'new_event':
+ return CalendarController::newEvent($json, $this->entityManager);
+ case 'update_event':
+ return CalendarController::updateEvent($json, $this->entityManager);
+ case 'remove_event':
+ return CalendarController::removeEvent($json, $this->entityManager);
+
+ /* -- mode maintenance -- */
+ case 'get_logs':
+ return MaintenanceController::getLogs($this->entityManager);
+ case 'erase_logs':
+ return MaintenanceController::eraseLogs($this->entityManager);
+
+ default:
+ return new JsonResponse(['success' => false, 'error' => 'bad parameters'], JsonResponse::HTTP_BAD_REQUEST);
+ }
+ }
+
+ /* -- site entier (header, footer, favicon) -- */
+ elseif($this->request->query->has('head_foot_text')){
+ return HeadFootController::setTextData($this->entityManager, $this->request->query->get('head_foot_text'), $json);
+ }
+ elseif($this->request->query->has('head_foot_social_check')){
+ return HeadFootController::displaySocialNetwork($this->entityManager, $this->request->query->get('head_foot_social_check'), $json);
+ }
+
+ /* -- page Menu et chemins -- */
+ elseif($this->request->query->has('menu_edit')){
+ // ne suit pas la règle, faire ça dans un contrôleur?
+ Model::$menu = new Menu($this->entityManager); // récupération des données
+
+ // flèche gauche <=: position = position du parent + 1, parent = grand-parent, recalculer les positions
+ if($this->request->query->get('menu_edit') === 'move_one_level_up' && isset($json['id'])){
+ return MenuAndPathsController::MoveOneLevelUp($this->entityManager, $json);
+ }
+ // flèche droite =>: position (léments de a fraterie + 1, l'élément précédent devient le parent
+ elseif($this->request->query->get('menu_edit') === 'move_one_level_down' && isset($json['id'])){
+ return MenuAndPathsController::MoveOneLevelDown($this->entityManager, $json);
+ }
+ elseif($this->request->query->get('menu_edit') === 'switch_positions' && isset($json['id1']) && isset($json['id2'])){
+ return MenuAndPathsController::switchPositions($this->entityManager, $json);
+ }
+ elseif($this->request->query->get('menu_edit') === 'display_in_menu' && isset($json['id']) && isset($json['checked'])){
+ return MenuAndPathsController::displayInMenu($this->entityManager, $json);
+ }
+ elseif($this->request->query->get('menu_edit') === 'url_edit' && isset($json['id']) && isset($json['field']) && isset($json['input_data'])){
+ return MenuAndPathsController::editUrl($this->entityManager, $json);
+ }
+ else{
+ return new JsonResponse(['success' => false, 'error' => 'bad parameters'], JsonResponse::HTTP_BAD_REQUEST); // code 400
+ }
+ }
+
+ /* -- mode Modification d'une page -- */
+ // partie "page"
+ elseif($this->request->query->has('page_edit')){
+ switch($this->request->query->get('page_edit')){
+ case 'page_title':
+ return PageManagementController::setPageTitle($this->entityManager, $json);
+ case 'page_description':
+ return PageManagementController::setPageDescription($this->entityManager, $json);
+ default:
+ return new JsonResponse(['success' => false, 'error' => 'bad parameters'], JsonResponse::HTTP_BAD_REQUEST); // code 400
+ }
+ }
+
+ // partie "blocs"
+ elseif($this->request->query->has('bloc_edit')){
+ switch($this->request->query->get('bloc_edit')){
+ case 'rename_page_bloc':
+ return PageManagementController::renameBloc($this->entityManager, $json);
+ case 'switch_blocs_positions':
+ return PageManagementController::SwitchBlocsPositions($this->entityManager, $json, $this->request->query->get('page'));
+ case 'change_articles_order':
+ return PageManagementController::changeArticlesOrder($this->entityManager, $json);
+ case 'change_presentation':
+ return PageManagementController::changePresentation($this->entityManager, $json);
+ case 'change_cols_min_width':
+ return PageManagementController::changeColsMinWidth($this->entityManager, $json);
+ case 'change_pagination_limit':
+ return PageManagementController::changePaginationLimit($this->entityManager, $json);
+ default:
+ return new JsonResponse(['success' => false, 'error' => 'bad parameters'], JsonResponse::HTTP_BAD_REQUEST); // code 400
+ }
+ }
+
+ else{
+ return new JsonResponse(['success' => false, 'error' => 'bad parameters'], JsonResponse::HTTP_BAD_REQUEST); // code 400
+ }
+ }
+
+ /* -- upload avec FormData OU formulaire HTML avec fichier -- */
+ elseif(str_starts_with($this->request->headers->get('Content-Type'), 'multipart/form-data')){ // = $_SERVER['CONTENT_TYPE']
+ // dans tinymce avec le plugin (bouton "insérer une image" de l'éditeur ou glisser-déposer)
+ if($this->request->query->get('action') === 'upload_image_tinymce'){
+ return ImageUploadController::imageUploadTinyMce();
+ }
+ // dans tinymce, des quatre méthodes: bouton "link", drag & drop, html, base64
+ elseif($this->request->query->get('action') === 'upload_file_tinymce'){
+ return FileUploadController::fileUploadTinyMce();
+ }
+ elseif($this->request->query->has('head_foot_image')){
+ return HeadFootController::uploadAsset($this->entityManager, $this->request->query->get('head_foot_image'));
+ }
+
+ /* -- page Maintenance -- */
+ elseif($this->request->query->get('action') === 'restore_database' && $this->request->request->get('hidden') === ''
+ && $this->request->files->has('uploaded_sql')){
+ return MaintenanceController::downloadSQL($this->entityManager, $this->request);
+ }
+ else{
+ // choix ici entre répondre en JSON ou par une redirection, choix du JSON pour pouvoir passer un message
+ return new JsonResponse(['success' => false, 'error' => 'bad parameters'], JsonResponse::HTTP_BAD_REQUEST); // code 400
+ }
+ }
+
+ /* -- formulaire HTML sans fichier -- */
+ elseif($this->request->headers->get('Content-Type') === 'application/x-www-form-urlencoded'){
+ if($this->request->query->get('action') === 'delete_article' && $this->request->query->has('id')){
+ return ArticleController::deleteArticle($this->entityManager, $this->request); // version formulaire
+ }
+
+ /* -- nouvelle page -- */
+ elseif($this->request->request->get('page_name') !== null
+ && $this->request->request->get('page_name_path') !== null
+ && $this->request->request->get('page_location') !== null
+ && $this->request->request->get('page_description') !== null
+ && $this->request->request->get('new_page_hidden') === ''){
+ return PageManagementController::newPage($this->entityManager, $this->request->request);
+ }
+
+ /* -- suppression d'une page -- */
+ elseif($this->request->request->get('page_id') !== null
+ && $this->request->request->get('submit_hidden') === ''){
+ return PageManagementController::deletePage($this->entityManager, $this->request->request->get('page_id'));
+ }
+
+
+ /* -- mode Modification d'une page -- */
+ // modification du chemins en snake_case
+ elseif($this->request->request->get('page_menu_path') !== null
+ && $this->request->request->get('page_id') !== null
+ && $this->request->request->get('page_name_path_hidden') === ''){
+ return PageManagementController::updatePageMenuPath($this->entityManager, $this->request->request->get('page_menu_path'));
+ }
+ // ajout d'un bloc dans une page
+ elseif($this->request->request->get('bloc_title') !== null
+ && $this->request->request->get('bloc_select') !== null
+ && $this->request->request->get('bloc_title_hidden') === ''){ // contrôle anti-robot avec input hidden
+ return PageManagementController::addBloc($this->entityManager, $this->request);
+ }
+ // suppression d'un bloc de page
+ elseif($this->request->request->get('delete_bloc_id') !== null
+ && $this->request->request->get('delete_bloc_hidden') === ''){ // contrôle anti-robot avec input hidden
+ return PageManagementController::deleteBloc($this->entityManager, $this->request);
+ }
+
+
+ /* -- page Menu et chemins -- */
+ // création d'une entrée de menu avec une URL
+ elseif($this->request->request->has("label_input") && $this->request->request->has("url_input") && $this->request->request->has("location")){
+ return MenuAndPathsController::newUrlMenuEntry($this->entityManager);
+ }
+ // suppression d'une entrée de menu avec une URL
+ elseif($this->request->request->has('delete') && $this->request->request->has('x') && $this->request->request->has('y')){ // 2 params x et y sont là parce qu'on a cliqué sur une image
+ return MenuAndPathsController::deleteUrlMenuEntry($this->entityManager);
+ }
+
+
+ /* -- page Mon compte -- */
+ elseif($this->request->query->get('action') === 'update_username'){
+ return UserController::updateUsername($this->entityManager);
+ }
+ elseif($this->request->query->get('action') === 'update_password'){
+ return UserController::updatePassword($this->entityManager);
+ }
+
+ /* -- page Maintenance -- */
+ elseif($this->request->query->get('action') === 'restore_database' && $this->request->get('hidden') === '' && $this->request->request->has('selected_sql')){
+ return MaintenanceController::handleBackupSelection($this->entityManager, $this->request);
+ }
+
+ // redirection page d'accueil
+ return new RedirectResponse((string)new URL(['error' => 'paramètres inconnus']));
+ }
+
+ // requêtes XMLHttpRequest
+ elseif($this->request->isXmlHttpRequest()){
+ return new JsonResponse(['success' => false]); // noyer le poisson en laissant penser que le site gère les requêtes XHR
+ }
+
+ // POST admin ne matchant pas
+ return new Response('bad parameters', Response::HTTP_BAD_REQUEST); // code 400
+ }
+
+ // POST non-admin ne matchant pas
+ return new Response('bad parameters', Response::HTTP_BAD_REQUEST); // code 400
+ }
+
+ // méthode HTTP inconnue
+ else{
+ return new RedirectResponse((string)new URL(['error' => 'tu fais quoi là mec?']));
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Service/Security.php b/src/Service/Security.php
new file mode 100644
index 0000000..0fbd8b7
--- /dev/null
+++ b/src/Service/Security.php
@@ -0,0 +1,112 @@
+1, // protection contre les élements et attributs dangereux
+
+ // liste blanche d'éléments HTML
+ 'elements'=> 'h1, h2, h3, h4, h5, h6, p, br, s, em, span, strong, a, ul, ol, li, sup, sub, code, blockquote, div, pre, table, caption, colgroup, col, tbody, tr, th, td, figure, img, figcaption, iframe, small',
+
+ // liste noire d'attributs HTML
+ 'deny_attribute'=> 'id, class' // on garde 'style'
+ );
+ // faire qu'un certain élément puisse n'avoir que certains attributs, regarder la doc
+ private static $specHtmLawed = '';
+
+ // obtenir du HTML non dangereur sans appliquer htmlspecialchars
+ public static function secureHTML(string $chaine): string
+ {
+ return trim(htmLawed($chaine, self::$configHtmLawed, self::$specHtmLawed));
+ }
+
+ public static function secureFileName(string $chaine): string
+ {
+ // sécuriser un nom avec chemin avec basename?
+ //$chaine = basename($chaine);
+
+ /*
+ - caractères interdits sous windows / \ : * ? " < > |
+ - mac autorise les /
+ - mac interdit :
+ - linux autorise tout sauf les /
+ - imagemagick ne supporte pas les :
+
+ - 'espace' fonctionne
+ - / remplacé par firefox en :
+ - \ retire ce qui est devant le \
+ - * fonctionne
+ - ? permet le téléchargement mais pas l'affichage
+ - " ne fonctionne pas, remplacé par %22, filtrer %22
+ - < > fonctionnent
+ - | fonctionne
+ - = fonctionne, mais je filtre parce qu'on en trouve dans une URL
+ - ' ` fonctionnent
+ - % fonctionne
+ - (){}[] fonctionnent
+ - ^ fonctionne
+ - # ne fonctionne pas
+ - ~ fonctionne
+ - & fonctionne
+ - ^ pas encore testé
+ */
+
+ // => on remplace tout par des _
+ // filtrer / et \ semble inutile
+
+ /*$cibles = [' ', '/', '\\', ':', '*', '?', '<', '>', '|', '=', "'", '`', '"', '%22', '#'];
+ $chaine = str_replace($cibles, '_', $chaine); // nécéssite l'extension mbstring
+ $chaine = mb_strtolower($chaine);
+ return($chaine);*/
+
+ $chaine = preg_replace('/[^a-zA-Z0-9_-]/', '_', $chaine); // ne garder que les lettres, chiffres, tirets et underscores
+ $chaine = preg_replace('/_+/', '_', $chaine); // doublons d'underscores
+ return trim($chaine, '_');
+
+ // les problèmes avec \ persistent !!
+ // => javascript
+ // malheureusement document.getElementById('upload').files[0].name = chaine; ne marche pas! interdit!
+ // javascript ne doit pas pouvoir accéder au système de fichiers
+ // solutions:
+ // - au lieu de fournir une chaine (le chemin du fichier), donner un objet à files[0].name
+ // - créer une copie du fichier et l'envoyer à la place
+ // - envoyer le fichier en AJAX
+ // - envoyer le nom du fichier à part puis renommer en PHP
+ }
+}
+
+// lien sans http://
+function fixLinks($data)
+{
+ // 1/
+ // si une adresse est de type "domaine.fr" sans le http:// devant, le comportement des navigateurs est de rechercher un fichier comme si mon adresse commençait par file://
+ // tomber ainsi sur une page d'erreur est parfaitement déroutant
+
+ // regex pour détecter les balises et ajouter http:// au début des liens si nécessaire
+ $pattern = '#(]+href=")((?!https?://)[^>]+>)#';
+ //$data = preg_replace($pattern, '$1http://$2', $data);
+
+ // 2/
+ // cas où la regex fait mal son boulot:
+ // l'erreur 404 est gérée par le .htaccess
+ // et le visiteur est redirigé à la page "menu"
+ // (ça ne règle pas le problème mais c'est mieux)
+
+ // 3/
+ // quand l'éditeur est ouvert (avant de valider l'article),
+ // le lien qu'on vient de créer apparaît dans l'infobulle,
+ // cliquer dessus ouvre un onglet sur une erreur 404
+ // solution partielle avec le .htaccess
+ //
+ // solution? fermer ce nouvel onglet avec echo '';
+ // comment déclencher le JS? en faisant qu'une erreur 404 causée pour cette raison soit particulière?
+
+ return($data);
+}
diff --git a/src/Service/URL.php b/src/Service/URL.php
new file mode 100644
index 0000000..035f572
--- /dev/null
+++ b/src/Service/URL.php
@@ -0,0 +1,90 @@
+params = $gets;
+ if($anchor != ''){
+ $this->setAnchor($anchor);
+ }
+ }
+
+ // setters statiques
+ static public function setProtocol(string $protocol = 'http'): void
+ {
+ self::$protocol = $protocol === 'https' ? 'https://' : 'http://';
+ }
+ static public function setPort(int|string $port = 80): void
+ {
+ if((int)$port === 443){
+ self::$protocol = 'https://';
+ self::$port = '';
+ }
+ elseif((int)$port === 80){
+ self::$protocol = 'http://';
+ self::$port = '';
+ }
+ else{
+ self::$port = ':' . (string)$port;
+ }
+ }
+ static public function setHost(string $host): void
+ {
+ self::$host = $host;
+ }
+ static public function setPath(string $path): void
+ {
+ self::$path = '/' . ltrim($path, '/');
+ }
+
+ //setters normaux
+ public function addParams(array $gets): void
+ {
+ // array_merge est préféré à l'opérateur d'union +, si une clé existe déjà la valeur est écrasée
+ $this->params = array_merge($this->params, $gets);
+ }
+ public function setAnchor(string $anchor = ''): void
+ {
+ if($anchor != ''){
+ $this->anchor = '#' . ltrim($anchor, '#');
+ }
+ else{
+ $this->anchor = '';
+ }
+ }
+
+ private function makeParams(): string
+ {
+ $output = '';
+ $first = true;
+
+ foreach($this->params as $key => $value) {
+ if($first){
+ $output .= '?';
+ $first = false;
+ }
+ else{
+ $output .= '&';
+ }
+ $output .= $key . '=' . $value;
+ }
+ return $output;
+ }
+
+ public function __toString(): string
+ {
+ return self::$protocol . self::$host . self::$port . self::$path . $this->makeParams() . $this->anchor;
+ }
+}
\ No newline at end of file
diff --git a/src/Service/User.php b/src/Service/User.php
new file mode 100644
index 0000000..bd4c315
--- /dev/null
+++ b/src/Service/User.php
@@ -0,0 +1,25 @@
+createQuery("SELECT u FROM App\Entity\User u")
+ ->setMaxResults(1)
+ ->getOneOrNullResult())
+ {
+ unset($_SESSION['user']);
+ return false;
+ }
+ else{
+ return true;
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Service/session.php b/src/Service/session.php
new file mode 100644
index 0000000..39c6ba2
--- /dev/null
+++ b/src/Service/session.php
@@ -0,0 +1,94 @@
+ 'session_invalide']));
+ die;
+ }
+
+ // MAJ de la session avec CERTAINES données
+ $_SESSION['user']['username'] = $user->getLogin();
+ $_SESSION['user']['role'] = $user->getRole();
+
+ $is_admin = $user->getRole() === 'admin';
+ }
+
+ define('IS_ADMIN', $is_admin);
+
+ // si on a un jour besoin d'une variable globale au lieu d'une constante
+ //$GLOBALS['is_admin'] = $is_admin; // version modifiable 1
+ /*function isAdmin(): bool { // version modifiable 2
+ return $_SESSION['user']['role'] ?? null === 'admin';
+ }*/
+
+
+ // => système de cache à ajouter pour ne pas lire la BDD à chaque fois
+ //remplacer ce qui est en haut
+ /*$user = $_SESSION['user'] ?? null;
+ if (!$user) {
+ // visiteur
+ }
+ // Vérification périodique (ex: toutes les 5 minutes)
+ if (time() - $user['last_check'] > 300) {
+ $user = UserController::getUserById($user['id'], $entityManager);
+ if (!$user) {
+ session_destroy();
+ header('Location: /login.php');
+ exit;
+ }
+ // cache pour ne pas avoir à lire la BDD à chaque page
+ $_SESSION['user'] = [
+ 'id' => $user['id'],
+ 'role' => $user['role'],
+ 'username' => $user['username'],
+ 'last_check' => time()
+ ];
+ $user = $_SESSION['user'];
+ }
+ $is_admin = ($user['role'] === 'admin');*/
+
+
+ // améliorations possibles: ajouter expiration automatique + protection contre vol de session (IP / user-agent) sans casser ton app.
+}
+
+// nettoyage complet
+/*function cleanSession(){
+ unset($_SESSION['user']); // mémoire vive
+ session_destroy(); // fichier côté serveur
+ setcookie('PHPSESSID', '', time() - 86400, '/'); // cookie de session
+}*/
\ No newline at end of file
--
cgit v1.2.3