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