From 3b369122645b07b290f7fcc7bccb4787745cd5ea Mon Sep 17 00:00:00 2001
From: polo
Date: Tue, 24 Mar 2026 22:39:29 +0100
Subject: =?UTF-8?q?mode=20maintenance,=20optimisation=20moins=20de=20contr?=
=?UTF-8?q?=C3=B4les=20en=20mode=20run,=20dossier=20service=20et=20d=C3=A9?=
=?UTF-8?q?placement=20fichiers,=20sessions=20et=20entit=C3=A9=20User=20pr?=
=?UTF-8?q?=C3=A9par=C3=A9es=20=C3=A0=20l'impl=C3=A9mentation=20hypoth?=
=?UTF-8?q?=C3=A9tique=20des=20r=C3=B4les,=20entit=C3=A9=20AppMetadata,=20?=
=?UTF-8?q?meilleure=20s=C3=A9curit=C3=A9=20de=20fillStartingDatabase?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
src/service/AppMode.php | 56 ++++++
src/service/Captcha.php | 55 ++++++
src/service/Config.php | 86 +++++++++
src/service/EmailService.php | 102 +++++++++++
src/service/FormValidation.php | 215 +++++++++++++++++++++++
src/service/Installation.php | 209 ++++++++++++++++++++++
src/service/Security.php | 110 ++++++++++++
src/service/URL.php | 88 ++++++++++
src/service/router.php | 384 +++++++++++++++++++++++++++++++++++++++++
src/service/session.php | 86 +++++++++
10 files changed, 1391 insertions(+)
create mode 100644 src/service/AppMode.php
create mode 100644 src/service/Captcha.php
create mode 100644 src/service/Config.php
create mode 100644 src/service/EmailService.php
create mode 100644 src/service/FormValidation.php
create mode 100644 src/service/Installation.php
create mode 100644 src/service/Security.php
create mode 100644 src/service/URL.php
create mode 100644 src/service/router.php
create mode 100644 src/service/session.php
(limited to 'src/service')
diff --git a/src/service/AppMode.php b/src/service/AppMode.php
new file mode 100644
index 0000000..60b58bd
--- /dev/null
+++ b/src/service/AppMode.php
@@ -0,0 +1,56 @@
+getRepository(AppMetadata::class)->find('mode');
+ if(!$metadata){
+ self::$mode = 'maintenance';
+ }
+ else{
+ self::$mode = $metadata->getValue();
+ }
+ }
+
+ public static function is(string $mode): bool
+ {
+ return self::$mode === $mode;
+ }
+
+ public static function get(): string
+ {
+ return self::$mode;
+ }
+
+ public static function set(EntityManager $entityManager, string $mode): void
+ {
+ self::$mode = $mode;
+
+ $metadata = $entityManager->find(AppMetadata::class, 'mode');
+ if($metadata){
+ $metadata->setValue($mode);
+ }
+ else{
+ $metadata = new AppMetadata('mode', $mode);
+ $entityManager->persist($metadata);
+ }
+ $entityManager->flush();
+
+ /*self::$data = [
+ 'mode' => $mode,
+ 'since' => (new DateTimeImmutable())->format('c'),
+ 'by' => $by,
+ ];*/
+ }
+}
\ No newline at end of file
diff --git a/src/service/Captcha.php b/src/service/Captcha.php
new file mode 100644
index 0000000..d57f912
--- /dev/null
+++ b/src/service/Captcha.php
@@ -0,0 +1,55 @@
+ on pourrait appliquer le pattern "singleton" (justification = le captcha devient une sorte de ressource partagée)
+
+declare(strict_types=1);
+
+class Captcha
+{
+ private int $a;
+ private int $b;
+
+ public function __construct(){
+ $this->a = rand(2, 9);
+ $this->b = rand(2, 9);
+ }
+
+ public function getA(): string
+ {
+ return $this->toLettersFrench($this->a);
+ }
+ public function getB(): string
+ {
+ return $this->toLettersFrench($this->b);
+ }
+ public function getSolution(): int
+ {
+ return ($this->a * $this->b);
+ }
+
+ private function toLettersFrench(int $number): string
+ {
+ return match($number){
+ 2 => 'deux',
+ 3 => 'trois',
+ 4 => 'quatre',
+ 5 => 'cinq',
+ 6 => 'six',
+ 7 => 'sept',
+ 8 => 'huit',
+ 9 => 'neuf',
+ default => '', // erreur
+ };
+ }
+
+ // (à déplacer dans FormValidation?)
+ static public function controlInput(string $input = '0'): int
+ {
+ // un POST est une chaîne qu'on doit convertir en nombre dans deux conditions:
+ // test de format: $input est un nombre
+ // test d'intégrité: supprimer les décimales avec (int) ne change pas la valeur du nombre
+ return is_numeric($input) && $input == (int)$input ? (int)$input : 0;
+ }
+}
\ No newline at end of file
diff --git a/src/service/Config.php b/src/service/Config.php
new file mode 100644
index 0000000..e59f728
--- /dev/null
+++ b/src/service/Config.php
@@ -0,0 +1,86 @@
+Le fichier config/config.ini n'existe pas ou n'est pas lisible.
";
+ }
+ define('TABLE_PREFIX', self::$table_prefix);
+ }
+
+ // renseigner les variables internes de Config
+ static private function hydrate(array $raw_data): void
+ {
+ foreach($raw_data as $field => $value)
+ {
+ if($value != '') // valeur par défaut
+ {
+ if(isset(self::$$field)) // le champ existe dans Config
+ {
+ // problème du slash à la fin du nom d'un dossier
+ $value = self::slashAtEndOfPath($field, $value);
+ self::$$field = $value;
+ }
+ else
+ {
+ echo "debug: le fichier config.ini comporte une erreur, le champ: " . $field . " est incorrect,\nl'information contenue sur cette ligne ne sera pas utilisée\n";
+ }
+ }
+ /*else
+ {
+ echo "debug: le champ " . $field . " est vide, la valeur par défaut " . self::$$field . " sera utilisée.\n";
+ }*/
+ }
+ }
+
+
+ // pour que les chemins finissent toujours par un /
+ static private function slashAtEndOfPath(string $field, string $value): string
+ {
+ foreach(self::$path_vars as $item)
+ {
+ if($field === $item){
+ return !str_ends_with($value, '/') ? $value . '/' : $value;
+ }
+ }
+ return $value;
+ }
+}
diff --git a/src/service/EmailService.php b/src/service/EmailService.php
new file mode 100644
index 0000000..6f4e93d
--- /dev/null
+++ b/src/service/EmailService.php
@@ -0,0 +1,102 @@
+ exceptions
+ $mail->CharSet = 'UTF-8';
+
+ $smtp_host = $form_data->getData()['smtp_host'] ?? Config::$smtp_host;
+ $smtp_secure = $form_data->getData()['smtp_secure'] ?? Config::$smtp_secure;
+ $smtp_username = $form_data->getData()['smtp_username'] ?? Config::$smtp_username;
+ $smtp_password = $form_data->getData()['smtp_password'] ?? Config::$smtp_password;
+ $email_from = $form_data->getData()['email_from'] ?? Config::$email_from; // une adresse bidon est donnée à setFrom()
+ $email_from_name = $form_data->getData()['email_from_name'] ?? Config::$email_from_name; // = site web
+ $email_dest = $form_data->getData()['email_dest'] ?? Config::$email_dest;
+ $email_dest_name = $form_data->getData()['email_dest_name'] ?? Config::$email_dest_name; // = destinataire formulaire
+
+ try{
+ // Paramètres du serveur
+ $mail->isSMTP();
+ $mail->Host = $smtp_host;
+ $mail->SMTPAuth = true;
+ $mail->Port = 25;
+
+ if($mail->SMTPAuth){
+ $mail->Username = $smtp_username; // e-mail
+ $mail->Password = $smtp_password;
+ $mail->SMTPSecure = $smtp_secure; // tls (starttls) ou ssl (smtps)
+ if($mail->SMTPSecure === 'tls'){
+ $mail->Port = 587;
+ }
+ elseif($mail->SMTPSecure === 'ssl'){
+ $mail->Port = 465;
+ }
+ }
+ //var_dump($mail->smtpConnect());die; // test de connexion
+
+ // Expéditeur et destinataire
+ // $email_from, $email_from_name et $email_dest_name sont modifiables uniquement dans le config.ini pour l'instant
+ $mail->setFrom(strtolower($email_from), $email_from_name);
+ $mail->addAddress(strtolower($email_dest), $email_dest_name);
+
+ // Contenu
+ $mail->isHTML(true);
+ if($test_email){
+ $mail->Subject = "TEST d'un envoi d'e-mail depuis le site web";
+ }
+ else{
+ $mail->Subject = 'Message envoyé par: ' . $name . ' (' . $email . ') depuis le site web';
+ }
+ $mail->Body = $message;
+ $mail->AltBody = $message;
+
+ $mail->send();
+
+ // copie en BDD
+ if(!$test_email && ($form_data->getData()['keep_emails'] ?? self::KEEP_EMAILS_DEFAULT)){
+ $db_email = new Email($name, $email, Config::$email_dest, $message, $form_data);
+ $entityManager->persist($db_email);
+ self::updateLastContactDate($entityManager, $email);
+ $entityManager->flush();
+ }
+
+ return true;
+ }
+ catch(Exception $e){
+ echo "Le message n'a pas pu être envoyé. Erreur : {$e}
{$mail->ErrorInfo}";
+ return false;
+ }
+ }
+
+ static public function updateLastContactDate(EntityManager $entityManager, string $sender): void
+ {
+ foreach($entityManager->getRepository('App\Entity\Email')->findAll() as $email){
+ $email->getSenderAddress() === $sender ? $email->updateLastContactDate() : null;
+ }
+ }
+
+ // peut être appelée par bin/clean_emails_cron.php
+ static public function cleanEmails(EntityManager $entityManager): void
+ {
+ $emails = $entityManager->getRepository('App\Entity\Email')->findAll();
+ foreach($emails as $email){
+ if($email->getDeletionDate() < new \DateTime()){
+ $entityManager->remove($email);
+ }
+ }
+ $entityManager->flush();
+ }
+}
\ No newline at end of file
diff --git a/src/service/FormValidation.php b/src/service/FormValidation.php
new file mode 100644
index 0000000..4677bef
--- /dev/null
+++ b/src/service/FormValidation.php
@@ -0,0 +1,215 @@
+data = $data;
+ $this->validation_strategy = $validation_strategy;
+ }
+
+ public function validate(): bool
+ {
+ $this->errors = [];
+
+ // pattern stratégie en une seule classe
+ switch($this->validation_strategy){
+ // bloc formulaire de contact
+ case 'email_send':
+ $this->emailStrategy();
+ break;
+ case 'email_params': // paramètrage en mode admin
+ $this->emailParamsStrategy();
+ break;
+
+ // formulaires pages spéciales
+ case 'create_user':
+ $this->createUserStrategy();
+ break;
+ case 'connection':
+ $this->connectionStrategy();
+ break;
+ case 'username_update':
+ $this->usernameUpdateStrategy();
+ break;
+ case 'password_update':
+ $this->passwordUpdateStrategy();
+ break;
+
+ default:
+ http_response_code(500); // c'est un peu comme jeter une exception
+ echo json_encode(['success' => false, 'error' => 'server_error']);
+ die;
+ }
+
+ $this->validated = true;
+ return empty($this->errors);
+ }
+
+ public function getErrors(): array
+ {
+ return $this->errors;
+ }
+
+ public function getField(string $field): string
+ {
+ return $this->validated ? $this->data[$field] : '';
+ }
+
+ // méthodes de validation
+ private function captchaValidate(bool $clean_session = true): void
+ {
+ $captcha_solution = (isset($_SESSION['captcha']) && is_int($_SESSION['captcha'])) ? $_SESSION['captcha'] : 0;
+ $captcha_try = isset($this->data['captcha']) ? Captcha::controlInput($this->data['captcha']) : 0;
+ if($clean_session){
+ unset($_SESSION['captcha']);
+ }
+
+ if($captcha_try == 0){
+ $error = 'error_non_valid_captcha';
+ }
+ elseif($captcha_solution == 0){ // ne peut pas arriver, si?
+ $error = 'captcha_server_error';
+ }
+ elseif($captcha_try !== $captcha_solution){
+ $this->errors[] = 'bad_solution_captcha';
+ }
+ }
+
+ // erreurs à la création des mots de passe
+ static private function removeSpacesTabsCRLF(string $chaine): string
+ {
+ $cibles = [' ', "\t", "\n", "\r"]; // doubles quotes !!
+ return(str_replace($cibles, '', $chaine));
+ }
+
+
+ // stratégies
+ private function emailStrategy(): void
+ {
+ $this->captchaValidate(false);
+
+ if(!isset($this->data['name']) || empty($this->data['name'])
+ || !isset($this->data['email']) || empty($this->data['email'])
+ || !isset($this->data['message']) || empty($this->data['message'])
+ || !isset($this->data['hidden']) || !empty($this->data['hidden'])){
+ $this->errors[] = 'missing_fields';
+ }
+
+ elseif(!filter_var(trim($this->data['email']), FILTER_VALIDATE_EMAIL)){
+ $this->errors[] = 'bad_email_address';
+ }
+
+ $this->data['name'] = htmlspecialchars(trim($this->data['name']));
+ $this->data['email'] = htmlspecialchars(trim($this->data['email']));
+ $this->data['message'] = htmlspecialchars($this->data['message']);
+ }
+ private function emailParamsStrategy(): void
+ {
+ if(!isset($this->data['id'], $this->data['what_param'], $this->data['value'], $this->data['hidden'])
+ || !empty($this->data['hidden'])){
+ $this->errors[] = 'missing_fields';
+ }
+
+ elseif($this->data['value'] !== ''){
+ if(!in_array($this->data['what_param'], ['smtp_host', 'smtp_secure', 'smtp_username', 'smtp_password', 'email_dest'])){
+ $this->errors[] = 'unknown_parameter';
+ }
+ elseif($this->data['what_param'] === 'smtp_username' || $this->data['what_param'] === 'email_dest'){
+ if(!filter_var($this->data['value'], FILTER_VALIDATE_EMAIL)){
+ $this->errors[] = 'invalide_email_address';
+ }
+ }
+ }
+
+ // htmlspecialchars exécutés à l'affichage dans FormBuilder
+ }
+ private function createUserStrategy(): void
+ {
+ $this->captchaValidate();
+
+ // test mauvais paramètres
+ if(!isset($this->data['login']) || empty($this->data['login'])
+ || !isset($this->data['password']) || empty($this->data['password'])
+ || !isset($this->data['password_confirmation']) || empty($this->data['password_confirmation'])
+ || !isset($this->data['create_user_hidden']) || !empty($this->data['create_user_hidden']))
+ {
+ $this->errors[] = 'bad_login_or_password';
+ }
+
+ if($this->data['password'] !== $this->data['password_confirmation']){
+ $this->errors[] = 'different_passwords';
+ }
+
+ if($this->data['login'] !== self::removeSpacesTabsCRLF(htmlspecialchars($this->data['login']))
+ || $this->data['password'] !== self::removeSpacesTabsCRLF(htmlspecialchars($this->data['password']))){
+ $this->errors[] = 'forbidden_characters';
+ }
+ }
+ private function connectionStrategy(): void
+ {
+ $this->captchaValidate();
+
+ if(!isset($this->data['login']) || empty($this->data['login'])
+ || !isset($this->data['password']) || empty($this->data['password'])
+ || !isset($this->data['connection_hidden']) || !empty($this->data['connection_hidden']))
+ {
+ $this->errors[] = 'bad_login_or_password';
+ }
+ }
+ private function usernameUpdateStrategy(): void
+ {
+ $this->captchaValidate();
+
+ if(!isset($this->data['login']) || empty($this->data['login'])
+ || !isset($this->data['password']) || empty($this->data['password'])
+ || !isset($this->data['new_login']) || empty($this->data['new_login'])
+ || !isset($this->data['modify_username_hidden']) || !empty($this->data['modify_username_hidden']))
+ {
+ $this->errors[] = 'bad_login_or_password';
+ }
+
+ $new_login = self::removeSpacesTabsCRLF(htmlspecialchars($this->data['new_login']));
+ if($new_login !== $this->data['new_login']){
+ $this->errors[] = 'forbidden_characters';
+ }
+
+ if($this->data['login'] !== $_SESSION['user']['username']){
+ $this->errors[] = 'bad_login_or_password';
+ }
+ if($this->data['login'] === $new_login){
+ $this->errors[] = 'same_username_as_before';
+ }
+ }
+ private function passwordUpdateStrategy(): void
+ {
+ $this->captchaValidate();
+
+ if(!isset($this->data['login']) || empty($this->data['login'])
+ || !isset($this->data['password']) || empty($this->data['password'])
+ || !isset($this->data['new_password']) || empty($this->data['new_password'])
+ || !isset($this->data['modify_password_hidden']) || !empty($this->data['modify_password_hidden']))
+ {
+ $this->errors[] = 'bad_login_or_password';
+ }
+
+ $new_password = self::removeSpacesTabsCRLF(htmlspecialchars($this->data['new_password']));
+ if($new_password !== $this->data['new_password']){
+ $this->errors[] = 'forbidden_characters';
+ }
+
+ if($this->data['login'] !== $_SESSION['user']['username']){
+ $this->errors[] = 'bad_login_or_password';
+ }
+ if($this->data['password'] === $new_password){
+ $this->errors[] = 'same_password_as_before';
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/service/Installation.php b/src/service/Installation.php
new file mode 100644
index 0000000..5c2a901
--- /dev/null
+++ b/src/service/Installation.php
@@ -0,0 +1,209 @@
+l'extension " . $extension . ' est manquante');
+ $flag = true;
+ }
+ }
+ if(!extension_loaded('imagick') && !extension_loaded('gd')){
+ echo("il manque une de ces extensions au choix pour le traitement des images: imagick (de préférence) ou gd.
");
+ $flag = true;
+ }
+ if($flag){
+ echo 'Réalisez les actions nécéssaires sur le serveur ou contactez l\'administrateur du site.
+ Quand le problème sera résolu, il vous suffira de recharger la page.
';
+ die;
+ }
+ }
+
+ static public function checkFilesAndFoldersRights(): void
+ {
+ // -- droits des fichiers et dossiers --
+ $droits_dossiers = 0700;
+ $droits_fichiers = 0600;
+
+ if(!file_exists('user_data')){
+ // créer le dossier user_data
+ mkdir('user_data/');
+ chmod('user_data/', $droits_dossiers);
+ echo 'Le dossier public/user_data introuvable et le serveur n\'a pas la permission de le créer.
+ Pour faire ça bien:
sudo -u "serveur web" mkdir /chemin/du/site/public/user_data
+ Aide: "serveur web" se nomme "www-data" sur debian et ubuntu, il s\'appelera "http" sur d\'autres distributions.
';
+ die;
+ }
+
+ if(!file_exists('../config/config.ini')){
+ // aide à la création du config.ini
+ echo 'Le fichier config/config.ini est introuvable.
';
+ echo 'Il doit obligatoirement contenir les codes de la base de données, le protocole http ou https (et éventuellement le port) utilisé pour créer les liens internes.
+ Un modèle est disponible, il s\'agit du fichier config/config-template.ini
+ Quand vous aurez terminé votre config.ini, donnez-lui par sécurité des droits 600.
';
+ die;
+ }
+ else{
+ // droits du config.ini
+ /*if(substr(sprintf('%o', fileperms('../config/config.ini')), -4) != 600){
+ chmod('../config/config.ini', $droits_fichiers);
+ }*/
+
+ // tester les liens internes
+ //
+
+ // le test de connexion à la BDD est dans le doctrine bootstrap
+ }
+ }
+
+
+ /* création d'un site minimal avec une page d'accueil à la toute 1ère visite du site, ne doit surtout pas être exécutée une seconde fois */
+
+ // protection 1 utilisé à chaque requête
+ static private function isFirstRun(EntityManager $entityManager): bool
+ {
+ $metadata = $entityManager->getRepository(AppMetadata::class)->find('installed');
+ return !$metadata || $metadata->getValue() !== '1';
+ }
+
+ // protection 2, qui vérifie vraiment que les tables concernées sont vides
+ static private function areTablesEmpty(EntityManager $entityManager): bool
+ {
+ $empty = true;
+ $entities = ['Page', 'Node', 'NodeData'];
+ foreach($entities as $entity){
+ $entity = 'App\Entity\\' . $entity; // nécéssaire quand on insère le nom avec une variable
+
+ if($entityManager
+ ->createQuery("SELECT e FROM $entity e")
+ ->setMaxResults(1)
+ ->getOneOrNullResult()){
+ $empty = false;
+ }
+ }
+
+ // cas anormal détecté, on remet en place la clé "installed"
+ if(!$empty){
+ self::preventReinstallation($entityManager);
+ }
+
+ return $empty;
+ }
+
+ // met en place la protection
+ static private function preventReinstallation(EntityManager $entityManager): void
+ {
+ $metadata = $entityManager->getRepository(AppMetadata::class)->find('installed');
+ if($metadata){
+ $metadata->setValue('1');
+ }
+ else{
+ $metadata = new AppMetadata('installed', '1');
+ $entityManager->persist($metadata);
+ }
+ $entityManager->flush();
+ }
+
+ static public function fillStartingDatabase(EntityManager $entityManager): void
+ {
+ if(!Installation::isFirstRun($entityManager)){
+ return;
+ }
+
+ // la BDD n'est pas vierge, on ne touche à rien
+ if(!self::areTablesEmpty($entityManager)){
+ return;
+ }
+
+ /* -- table page -- */
+ // paramètres: name_page, end_of_path, reachable, in_menu, hidden, position, parent
+ $accueil = new Page('Accueil', 'accueil', "Page d'accueil", true, true, false, 1, NULL);
+ $article = new Page('Article', 'article', "", true, false, false, NULL, NULL);
+ $connection = new Page('Connexion', 'connection', "Connexion", true, false, false, NULL, NULL);
+ $my_account = new Page('Mon compte', 'user_edit', "Mon compte", true, false, false, NULL, NULL);
+ $menu_paths = new Page("Menu et chemins", 'menu_paths', "Menu et chemins", true, false, false, NULL, NULL);
+ $menu_paths->addCSS('menu');
+ $menu_paths->addJS('menu');
+ $new_page = new Page('Nouvelle page', 'new_page', "Nouvelle page", true, false, false, NULL, NULL);
+ $new_page->addCSS('new_page');
+ $new_page->addJS('new_page');
+ $emails = new Page("Courriels", 'emails', "Consulter les courriels en base de données", true, false, false, NULL, NULL);
+ $emails->addCSS('show_emails');
+ $emails->addJS('form');
+
+ /* -- table node -- */
+ // paramètres: name_node, article_timestamp, attributes, position, parent, page, article
+ $head = new Node('head', 1, NULL, NULL, NULL);
+ $header = new Node('header', 2, NULL, NULL, NULL);
+ $nav = new Node('nav', 1, $header, NULL, NULL);
+ $main = new Node('main', 3, NULL, NULL, NULL);
+ $footer = new Node('footer', 4, NULL, NULL, NULL);
+ $breadcrumb = new Node('breadcrumb', 2, $header, NULL, NULL);
+ $login = new Node('login', 1, $main, $connection, NULL);
+ $user_edit = new Node('user_edit', 1, $main, $my_account, NULL);
+ $bloc_edit_menu = new Node('menu', 1, $main, $menu_paths, NULL);
+ $bloc_new_page = new Node('new_page', 1, $main, $new_page, NULL);
+ $bloc_emails = new Node('show_emails', 1, $main, $emails, NULL);
+
+ /* -- table node_data -- */
+ // paramètres: data, node, images
+ $head_data = new NodeData([], $head);
+ $header_data = new NodeData([], $header);
+ $footer_data = new NodeData([], $footer);
+ $emails_data = new NodeData([], $bloc_emails);
+
+ /* -- table page -- */
+ $entityManager->persist($accueil);
+ $entityManager->persist($article);
+ $entityManager->persist($connection);
+ $entityManager->persist($my_account);
+ $entityManager->persist($menu_paths);
+ $entityManager->persist($new_page);
+ $entityManager->persist($emails);
+
+ /* -- table node -- */
+ $entityManager->persist($head);
+ $entityManager->persist($header);
+ $entityManager->persist($nav);
+ $entityManager->persist($main);
+ $entityManager->persist($footer);
+ $entityManager->persist($breadcrumb);
+ $entityManager->persist($login);
+ $entityManager->persist($user_edit);
+ $entityManager->persist($bloc_edit_menu);
+ $entityManager->persist($bloc_new_page);
+ $entityManager->persist($bloc_emails);
+
+ /* -- table node_data -- */
+ $entityManager->persist($head_data);
+ $entityManager->persist($header_data);
+ $entityManager->persist($footer_data);
+ $entityManager->persist($emails_data);
+
+ $entityManager->flush();
+
+ // empêcher la réutilisation de cette fonction
+ self::preventReinstallation($entityManager);
+
+ // fin de l'installation
+ AppMode::set($entityManager, 'run');
+
+ // recharger la page?
+ //header('Location: ' . new URL);
+ }
+}
\ No newline at end of file
diff --git a/src/service/Security.php b/src/service/Security.php
new file mode 100644
index 0000000..356f4f4
--- /dev/null
+++ b/src/service/Security.php
@@ -0,0 +1,110 @@
+1, // protection contre les élements et attributs dangereux
+
+ // liste blanche d'éléments HTML
+ 'elements'=> 'h1, h2, h3, h4, h5, h6, p, br, s, em, span, strong, a, ul, ol, li, sup, sub, code, blockquote, div, pre, table, caption, colgroup, col, tbody, tr, th, td, figure, img, figcaption, iframe, small',
+
+ // liste noire d'attributs HTML
+ 'deny_attribute'=> 'id, class' // on garde 'style'
+ );
+ // faire qu'un certain élément puisse n'avoir que certains attributs, regarder la doc
+ private static $specHtmLawed = '';
+
+ // obtenir du HTML non dangereur sans appliquer htmlspecialchars
+ public static function secureHTML(string $chaine): string
+ {
+ return trim(htmLawed($chaine, self::$configHtmLawed, self::$specHtmLawed));
+ }
+
+ public static function secureFileName(string $chaine): string
+ {
+ // sécuriser un nom avec chemin avec basename?
+ //$chaine = basename($chaine);
+
+ /*
+ - caractères interdits sous windows / \ : * ? " < > |
+ - mac autorise les /
+ - mac interdit :
+ - linux autorise tout sauf les /
+ - imagemagick ne supporte pas les :
+
+ - 'espace' fonctionne
+ - / remplacé par firefox en :
+ - \ retire ce qui est devant le \
+ - * fonctionne
+ - ? permet le téléchargement mais pas l'affichage
+ - " ne fonctionne pas, remplacé par %22, filtrer %22
+ - < > fonctionnent
+ - | fonctionne
+ - = fonctionne, mais je filtre parce qu'on en trouve dans une URL
+ - ' ` fonctionnent
+ - % fonctionne
+ - (){}[] fonctionnent
+ - ^ fonctionne
+ - # ne fonctionne pas
+ - ~ fonctionne
+ - & fonctionne
+ - ^ pas encore testé
+ */
+
+ // => on remplace tout par des _
+ // filtrer / et \ semble inutile
+
+ /*$cibles = [' ', '/', '\\', ':', '*', '?', '<', '>', '|', '=', "'", '`', '"', '%22', '#'];
+ $chaine = str_replace($cibles, '_', $chaine); // nécéssite l'extension mbstring
+ $chaine = mb_strtolower($chaine);
+ return($chaine);*/
+
+ $chaine = preg_replace('/[^a-zA-Z0-9_-]/', '_', $chaine); // ne garder que les lettres, chiffres, tirets et underscores
+ $chaine = preg_replace('/_+/', '_', $chaine); // doublons d'underscores
+ return trim($chaine, '_');
+
+ // les problèmes avec \ persistent !!
+ // => javascript
+ // malheureusement document.getElementById('upload').files[0].name = chaine; ne marche pas! interdit!
+ // javascript ne doit pas pouvoir accéder au système de fichiers
+ // solutions:
+ // - au lieu de fournir une chaine (le chemin du fichier), donner un objet à files[0].name
+ // - créer une copie du fichier et l'envoyer à la place
+ // - envoyer le fichier en AJAX
+ // - envoyer le nom du fichier à part puis renommer en PHP
+ }
+}
+
+// lien sans http://
+function fixLinks($data)
+{
+ // 1/
+ // si une adresse est de type "domaine.fr" sans le http:// devant, le comportement des navigateurs est de rechercher un fichier comme si mon adresse commençait par file://
+ // tomber ainsi sur une page d'erreur est parfaitement déroutant
+
+ // regex pour détecter les balises 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..5bd2594
--- /dev/null
+++ b/src/service/URL.php
@@ -0,0 +1,88 @@
+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/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 @@
+ 1er test, méthode http: GET, POST ou autre chose
+=> 2ème test, type de contenu (méthode POST uniquement):
+"application/x-www-form-urlencoded" = formulaire
+"application/json" = requête AJAX avec fetch()
+"multipart/form-data" = upload d'image par tinymce
+$_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest' requête AJAX xhs, non utilisée
+=> 3ème test, comme le 2ème test mais uniquement si IS_ADMIN est vrai
+*/
+
+declare(strict_types=1);
+
+if($request->getMethod() === 'GET'){
+ // table "user" vide
+ if(!UserController::existUsers($entityManager)){
+ require AbstractBuilder::VIEWS_PATH . 'user_create.php';
+ die;
+ }
+
+ // bouton déconnexion (méthode GET parce que l'utilisateur ne modifie plus de données à partir de là)
+ if($request->query->has('action') && $request->query->get('action') === 'deconnection'){
+ UserController::disconnect($entityManager);
+ }
+
+ // articles suivants
+ if($request->query->has('fetch') && $request->query->get('fetch') === 'next_articles'){
+ ArticleController::fetch($entityManager, $request);
+ }
+
+ // données du calendrier
+ // création du calendrier et changement de dates affichées (boutons flèches mais pas changement de vue)
+ if($request->query->has('action') && $request->query->get('action') === 'get_events'
+ && $request->query->has('start') && $request->query->has('end') && empty($request->getPayload()->all())) // getPayload ne récupère pas que des POST
+ {
+ CalendarController::getData($entityManager);
+ }
+
+ // pages interdites
+ if(!IS_ADMIN && in_array(CURRENT_PAGE, ['menu_paths', 'new_page', 'user_edit', 'emails'])){
+ header('Location: ' . new URL);
+ die;
+ }
+
+ if(IS_ADMIN === true){
+ // ...
+ }
+
+ // construction d'une page
+ $response = (new ViewController)->buildView($entityManager, $request); // utilise Model
+ // parenthèses nécéssaires autour de l'instanciation pour PHP < 8.4
+}
+
+
+elseif($request->getMethod() === 'POST'){
+ /* -- contrôleurs appellables par tout le monde -- */
+
+ // table "user" vide
+ if(!UserController::existUsers($entityManager)){
+ UserController::createAdminUser($entityManager);
+ }
+
+ // requêtes JSON avec fetch()
+ if($_SERVER['CONTENT_TYPE'] === 'application/json')
+ {
+ $json = json_decode($request->getContent(), true); // = json_decode(file_get_contents('php://input'), true);
+
+ if(isset($_GET['action']))
+ {
+ // formulaire de contact
+ if($_GET['action'] === 'send_email'){
+ ContactFormController::sendVisitorEmail($entityManager, $json);
+ }
+ }
+ }
+
+ // envoi formulaire HTML
+ elseif($_SERVER['CONTENT_TYPE'] === 'application/x-www-form-urlencoded'){
+ // tentative de connexion
+ if($request->query->has('action') && $request->query->get('action') === 'connection'){
+ //$response =
+ UserController::connect($entityManager);
+ }
+ }
+
+
+ if(IS_ADMIN === true)
+ {
+ /* -- requêtes AJAX -- */
+
+ // requêtes JSON avec fetch()
+ if($_SERVER['CONTENT_TYPE'] === 'application/json')
+ {
+ $json = json_decode($request->getContent(), true); // = json_decode(file_get_contents('php://input'), true);
+
+ if($request->query->has('action'))
+ {
+ /* -- manipulation des articles -- */
+ if($_GET['action'] === 'editor_submit' && isset($json['id']) && isset($json['content'])){
+ ArticleController::editorSubmit($entityManager, $json);
+ }
+ elseif($_GET['action'] === 'delete_article' && isset($json['id'])){
+ $response = ArticleController::deleteArticle($entityManager, $json); // version AJAX
+ }
+ elseif($_GET['action'] === 'switch_positions' && isset($json['id1']) && isset($json['id2'])){
+ ArticleController::switchPositions($entityManager, $json);
+ }
+ elseif($_GET['action'] === 'date_submit' && isset($json['id']) && isset($json['date'])){
+ ArticleController::dateSubmit($entityManager, $json);
+ }
+
+ /* -- bloc Formulaire -- */
+ elseif($_GET['action'] === 'keep_emails'){
+ ContactFormController::keepEmails($entityManager, $json);
+ }
+ elseif($_GET['action'] === 'set_retention_period'){
+ ContactFormController::setEmailsRetentionPeriod($entityManager, $json);
+ }
+ elseif($_GET['action'] === 'set_email_param'){
+ ContactFormController::setEmailParam($entityManager, $json);
+ }
+ elseif($_GET['action'] === 'test_email'){
+ ContactFormController::sendTestEmail($entityManager, $json);
+ }
+
+ /* -- page emails -- */
+ elseif($_GET['action'] === 'delete_email'){
+ ContactFormController::deleteEmail($entityManager, $json);
+ }
+ elseif($_GET['action'] === 'toggle_sensitive_email'){
+ ContactFormController::toggleSensitiveEmail($entityManager, $json);
+ }
+
+ /* -- upload d'image dans tinymce par copier-coller -- */
+ // collage de HTML contenant une ou plusieurs balises
+ elseif($request->query->get('action') === 'upload_image_url'){
+ ImageUploadController::uploadImageHtml();
+ }
+ // collage d'une image (code base64 dans le presse-papier) non encapsulée dans du HTML
+ elseif($request->query->get('action') === 'upload_image_base64'){
+ ImageUploadController::uploadImageBase64();
+ }
+
+
+ /* -- requêtes spécifiques au calendrier -- */
+ elseif($request->query->get('action') === 'new_event'){
+ CalendarController::newEvent($json, $entityManager);
+ }
+ elseif($request->query->get('action') === 'update_event'){
+ CalendarController::updateEvent($json, $entityManager);
+ }
+ elseif($request->query->get('action') === 'remove_event'){
+ CalendarController::removeEvent($json, $entityManager);
+ }
+ else{
+ echo json_encode(['success' => false]);
+ die;
+ }
+ }
+
+ /* -- site entier (header, footer, favicon) -- */
+ elseif($request->query->has('head_foot_text')){
+ HeadFootController::setTextData($entityManager, $request->query->get('head_foot_text'), $json);
+ }
+ elseif($request->query->has('head_foot_social_check')){
+ HeadFootController::displaySocialNetwork($entityManager, $request->query->get('head_foot_social_check'), $json);
+ }
+
+ /* -- page Menu et chemins -- */
+ elseif(isset($_GET['menu_edit']))
+ {
+ // ne suit pas la règle, faire ça dans un contrôleur?
+ Model::$menu = new Menu($entityManager); // récupération des données
+
+ // flèche gauche <=: position = position du parent + 1, parent = grand-parent, recalculer les positions
+ if($_GET['menu_edit'] === 'move_one_level_up' && isset($json['id'])){
+ MenuAndPathsController::MoveOneLevelUp($entityManager, $json);
+ }
+ // flèche droite =>: position = nombre d'éléments de la fraterie + 1, l'élément précédent devient le parent
+ elseif($_GET['menu_edit'] === 'move_one_level_down' && isset($json['id'])){
+ MenuAndPathsController::MoveOneLevelDown($entityManager, $json);
+ }
+ elseif($_GET['menu_edit'] === 'switch_positions' && isset($json['id1']) && isset($json['id2'])){
+ MenuAndPathsController::switchPositions($entityManager, $json);
+ }
+ elseif($_GET['menu_edit'] === 'display_in_menu' && isset($json['id']) && isset($json['checked'])){
+ MenuAndPathsController::displayInMenu($entityManager, $json);
+ }
+ elseif($_GET['menu_edit'] === 'url_edit' && isset($json['id']) && isset($json['field']) && isset($json['input_data'])){
+ MenuAndPathsController::editUrl($entityManager, $json);
+ }
+ }
+
+ /* -- mode Modification d'une page -- */
+ // partie "page"
+ elseif(isset($_GET['page_edit']))
+ {
+ // titre de la page
+ if($_GET['page_edit'] === 'page_title'){
+ PageManagementController::setPageTitle($entityManager, $json);
+ }
+ // description dans les métadonnées
+ elseif($_GET['page_edit'] === 'page_description'){
+ PageManagementController::setPageDescription($entityManager, $json);
+ }
+ }
+
+ // partie "blocs"
+ elseif($request->query->has('bloc_edit'))
+ {
+ if($request->query->get('bloc_edit') === 'rename_page_bloc'){
+ PageManagementController::renameBloc($entityManager, $json);
+ }
+ elseif($request->query->get('bloc_edit') === 'switch_blocs_positions'){
+ PageManagementController::SwitchBlocsPositions($entityManager, $json);
+ }
+ elseif($request->query->get('bloc_edit') === 'change_articles_order'){
+ PageManagementController::changeArticlesOrder($entityManager, $json);
+ }
+ elseif($request->query->get('bloc_edit') === 'change_presentation'){
+ PageManagementController::changePresentation($entityManager, $json);
+ }
+ elseif($request->query->get('bloc_edit') === 'change_cols_min_width'){
+ PageManagementController::changeColsMinWidth($entityManager, $json);
+ }
+ elseif($request->query->get('bloc_edit') === 'change_pagination_limit'){
+ PageManagementController::changePaginationLimit($entityManager, $json);
+ }
+ }
+ }
+
+ // upload avec FormData
+ elseif(strpos($_SERVER['CONTENT_TYPE'], 'multipart/form-data') !== false)
+ {
+ // dans tinymce avec le plugin (bouton "insérer une image" de l'éditeur ou glisser-déposer)
+ if($request->query->has('action') && $request->query->get('action') === 'upload_image_tinymce'){
+ ImageUploadController::imageUploadTinyMce();
+ }
+ // dans tinymce, des quatre méthodes: bouton "link", drag & drop, html, base64
+ elseif($request->query->has('action') && $request->query->get('action') === 'upload_file_tinymce'){
+ FileUploadController::fileUploadTinyMce();
+ }
+ elseif($request->query->has('head_foot_image')){
+ HeadFootController::uploadAsset($entityManager, $request->query->get('head_foot_image'));
+ }
+ }
+
+ // requêtes XMLHttpRequest
+ elseif(isset($_SERVER['HTTP_X_REQUESTED_WITH']) && $_SERVER['HTTP_X_REQUESTED_WITH'] == 'XMLHttpRequest')
+ {
+ //echo "requête XMLHttpRequest reçue par le serveur";
+ echo json_encode(['success' => false]); // noyer le poisson en laissant penser que le site gère les requêtes XHR
+ die;
+ }
+
+ /* -- envoi formulaire HTML -- */
+ elseif($_SERVER['CONTENT_TYPE'] === 'application/x-www-form-urlencoded')
+ {
+ if($request->query->has('action') && $request->query->get('action') === 'delete_article' && isset($_GET['id'])){
+ $response = ArticleController::deleteArticle($entityManager, $_GET); // version formulaire
+ }
+
+ /* -- nouvelle page -- */
+ elseif(isset($_POST['page_name']) && $_POST['page_name'] !== null
+ && isset($_POST['page_name_path']) && $_POST['page_name_path'] !== null
+ && isset($_POST['page_location']) && $_POST['page_location'] !== null
+ && isset($_POST['page_description']) && $_POST['page_description'] !== null
+ && isset($_POST['new_page_hidden']) && $_POST['new_page_hidden'] === '')
+ {
+ PageManagementController::newPage($entityManager, $_POST);
+ }
+
+ /* -- suppression d'une page -- */
+ elseif(isset($_POST['page_id']) && $_POST['page_id'] !== null
+ && isset($_POST['submit_hidden']) && $_POST['submit_hidden'] === '')
+ {
+ PageManagementController::deletePage($entityManager);
+ }
+
+
+ /* -- mode Modification d'une page -- */
+
+ // modification du chemins en snake_case
+ elseif(isset($_POST['page_menu_path']) && $_POST['page_menu_path'] !== null
+ && isset($_POST['page_id']) && $_POST['page_id'] !== null
+ && isset($_POST['page_name_path_hidden']) && $_POST['page_name_path_hidden'] === '')
+ {
+ PageManagementController::updatePageMenuPath($entityManager);
+ }
+ // ajout d'un bloc dans une page
+ elseif(isset($_POST['bloc_title']) && $_POST['bloc_title'] !== null
+ && isset($_POST['bloc_select']) && $_POST['bloc_select'] !== null
+ && isset($_POST['bloc_title_hidden']) && $_POST['bloc_title_hidden'] === '') // contrôle anti-robot avec input hidden
+ {
+ PageManagementController::addBloc($entityManager);
+ }
+ // suppression d'un bloc de page
+ elseif(isset($_POST['delete_bloc_id']) && $_POST['delete_bloc_id'] !== null
+ && isset($_POST['delete_bloc_hidden']) && $_POST['delete_bloc_hidden'] === '') // contrôle anti-robot avec input hidden
+ {
+ PageManagementController::deleteBloc($entityManager);
+ }
+
+
+ /* -- page Menu et chemins -- */
+
+ // création d'une entrée de menu avec une URL
+ elseif(isset($_POST["label_input"]) && isset($_POST["url_input"]) && isset($_POST["location"])){
+ MenuAndPathsController::newUrlMenuEntry($entityManager);
+ }
+ // suppression d'une entrée de menu avec une URL
+ elseif(isset($_POST['delete']) && isset($_POST['x']) && isset($_POST['y'])){ // 2 params x et y sont là parce qu'on a cliqué sur une image
+ MenuAndPathsController::deleteUrlMenuEntry($entityManager);
+ }
+
+
+ /* -- page Mon compte -- */
+ elseif($request->query->has('action') && $request->query->get('action') === 'update_username')
+ {
+ UserController::updateUsername($entityManager);
+ }
+ elseif($request->query->has('action') && $request->query->get('action') === 'update_password')
+ {
+ UserController::updatePassword($entityManager);
+ }
+
+ // redirection page d'accueil
+ else{
+ header("Location: " . new URL(['error' => 'paramètres inconnus']));
+ die;
+ }
+ }
+ // POST admin ne matchant pas
+ else{
+ echo json_encode(['success' => false]);
+ die;
+ }
+ }
+ // POST non admin ne matchant pas
+ else{
+ echo json_encode(['success' => false]);
+ die;
+ }
+}
+
+// méthode inconnue
+else{
+ header("Location: " . new URL(['error' => 'tu fais quoi là mec?']));
+ die;
+}
+
+
+
+/* -- utilisation de la réponse -- */
+if(isset($response)){
+ // cas gérés (d'autres sont à prévoir): mauvais id de la page article, accès page création d'article sans être admin
+ if($request->isMethod('GET') && $response->getStatusCode() == 302){ // 302 redirection temporaire
+ header('Location: ' . new URL(['page' => $_GET['from'] ?? '']));
+ }
+ // redirection après traitement de formulaires HTTP
+ elseif($request->getMethod() === 'POST' && $_SERVER['CONTENT_TYPE'] === 'application/x-www-form-urlencoded'){
+ $response_data = json_decode(($response)->getContent(), true);
+ $url = new URL(['page' => $_GET['from'] ?? '']);
+ $url->addParams(['success' => $response_data['success'], 'message' => $response_data['message']]);
+ header('Location: ' . $url);
+ }
+ // affichage d'une page OU requête AJAX
+ else{
+ $response->send();
+ }
+}
+// pas utilisation de RESPONSE (cas destiné à disparaître)
+else{
+ if($request->getMethod() === 'POST' && $_SERVER['CONTENT_TYPE'] === 'application/x-www-form-urlencoded'){
+ header("Location: " . new URL(['error' => 'erreur côté serveur']));
+ }
+ else{
+ http_response_code(500);
+ echo "erreur côté serveur";
+ }
+}
+//die; // inutile
\ No newline at end of file
diff --git a/src/service/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 @@
+ '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