TD7 – Cookies & sessions Panier et préférences

HTTP est un protocole de communication avec lequel chaque requête-réponse est indépendante l’une de l’autre. Du coup, le serveur n’a pas de moyen de reconnaître un client particulier, et donc n’a pas de moyen d’enregistrer d’informations liées à un client spécifique. Par exemple, avec le HTTP de base, si vous allez plusieurs fois sur Facebook, le réseau social ne sait pas reconnaître vos requêtes parmi toutes les requêtes qu’il reçoit. Donc il ne peut pas vous connecter, afficher votre page personnelle…

Pour remédier à cela, HTTP prévoit le mécanisme des cookies qui permet d’enregistrer des informations sur l’ordinateur du client. De plus, en utilisant des cookies pour identifier ses clients, les serveurs PHP peuvent stocker côté serveur des informations spécifiques à un client : c’est le mécanisme des sessions.

Les prochains TDs nécessitent que votre contrôleur utilisateur puisse créer, lire, mettre à jour et supprimer des utilisateurs. Il faut donc finir d’abord le TD6. Si vous bloquez sur la section Modèle générique du TD6, vous pouvez soit demander de l’aide à votre enseignant, soit adapter VoitureRepository pour que UtilisateurRepository fonctionne.

Les cookies

Un cookie est utilisé pour stocker quelques informations spécifiques à un utilisateur comme :

Les informations sont envoyées par le site (serveur HTTP) en même temps que la page Web. Le client stocke ces informations sur sa machine dans un fichier appelé cookie : il s’agit d’une table d’associations nom/valeur.

Attention : il ne faut pas stocker de données critiques dans les cookies, car elles sont stockées telles quelles sur le disque dur du client !

Les cookies sont des informations stockées sur l’ordinateur du client à l’initiative du serveur.

D’un point de vue pratique en PHP, on dépose un cookie à l’aide de la fonction setcookie. Par exemple, la ligne ci-dessous crée un cookie nommé TestCookie contenant la valeur "OK" et qui expire dans 1h.

setcookie("TestCookie", "OK", time() + 3600);  
/* expire dans 1 heure = 3600 secondes */

D’un point de vue technique, voici ce qui se passe au niveau du protocole HTTP (dont nous avons notamment parlé lors du cours 1). Pour stocker des informations dans un cookie chez le client, le serveur écrit des lignes Set-Cookie dans l’en-tête de sa réponse HTTP

HTTP/1.1 200 OK
Date:Thu, 22 Oct 2015 15:43:27 GMT
Server: Apache/2.2.14 (Ubuntu)
Accept-Ranges: bytes
Content-Length: 5781
Content-Type: text/html
Set-Cookie: TestCookie1=valeur1; expires=Thu, 22-Oct-2015 16:43:27 GMT; Max-Age=3600
Set-Cookie: TestCookie2=valeur2; expires=Thu, 22-Oct-2015 16:43:27 GMT; Max-Age=3600

<html><head>...

Remarquons que le serveur écrit une ligne Set-Cookie par paire nom/valeur. Ici nous avons un cookie "TestCookie1" de valeur "valeur1" et un cookie "TestCookie2" de valeur "valeur2".

  1. Créez une action deposerCookie dans le contrôleur utilisateur. Cette action doit déposer un cookie de votre choix.

  2. Vous allez inspecter la réponse HTTP de votre serveur pour observer l’explication précédente :

    • Allez dans les outils développeurs (avec F12) → Onglet Réseau (ou Network).
    • Rechargez votre page Web qui enregistre un cookie.
    • En cliquant sur la requête de controleurFrontal.php, vous pouvez voir les en-têtes (ou Headers) de la réponse et y observer la ligne Set-Cookie: ....
  3. Observez le cookie déposé chez le client :

    • sous Firefox, allez dans les outils développeurs (avec F12) → Onglet Stockage (ou Application) → Cookies.
    • sous Chrome, allez dans les outils développeurs (avec F12) → Onglet Ressources (ou Application) → puis Storage et Cookies.

    Note : Il existe aussi un sous-onglet de Réseau pour voir les cookies correspondants à une requête.

À chaque requête HTTP, le navigateur du client envoie ses cookies correspondant au site visité dans l’en-tête de la requête.

Comment le client transmet-il les informations de ses cookies ?

D’un point de vue technique, voici ce qui se passe au niveau du protocole HTTP. Le client envoie les informations de ses cookies dans l’en-tête de ses requêtes.

GET /~rletud/index.html HTTP/1.1
Host: webinfo.iutmontp.univ-montp2.fr
Cookie: TestCookie1=valeur1; TestCookie2=valeur2

Comment le serveur peut-il lire en PHP les cookies envoyés par le client ?

Le PHP traite la requête pour rendre le cookie disponible dans la variable $_COOKIE, de la même manière que $_GET récupère l’information dans l’URL et que $_POST récupère l’information dans le corps de la requête (cf. le cours 1).

Par exemple,

echo $_COOKIE["TestCookie1"];

devrait afficher valeur1.

  1. Créez une action lireCookie dans le contrôleur utilisateur. Cette action doit lire le cookie précédemment déposé et l’afficher.

  2. Inspectez la requête HTTP de votre client pour observer l’explication précédente. Dans l’onglet Réseau des outils développeurs, regarder les en-têtes (ou Headers) de la requête et y observer la ligne Cookie: ....

Notes techniques

  1. Les cookies ne peuvent contenir que des valeurs string, donc a priori pas des objets PHP. Il faut donc convertir en chaîne de caractères les autres variables PHP avant de les stocker :

    • La fonction serialize permet de transformer une variable en chaîne de caractère.

    • Inversement, il faut appliquer unserialize pour récupérer la variable PHP à partir de sa chaîne de caractère sérialisée. On applique donc unserialize lorsque l’on récupère la valeur stockée dans le cookie.

    Avertissement : La fonction unserialize peut poser des problèmes de sécurité. Il ne faut donc pas l’utiliser telle quelle dans un site professionnel.

  2. Si vous ne spécifiez pas le temps d’expiration d’un cookie (3ème paramètre de setcookie) ou que vous le mettez à 0 alors le cookie sera supprimé à la fin de la session (lorsque le navigateur sera fermé).

Nous allons regrouper toutes les fonctionnalités des cookies dans une classe.

  1. Créez la classe Cookie dans le fichier src/Modele/HTTP/Cookie.php en y indiquant le bon espace de nom.
  2. Codez la méthode
    public static function enregistrer(string $cle, mixed $valeur, ?int $dureeExpiration = null): void
    

    Note :

    • Pour pouvoir stocker tout type de valeur, transformez-la toujours en chaîne de caractères avant de la stocker.
    • $dureeExpiration indique dans combien de secondes est-ce que le cookie doit expirer.
    • Il faut traiter séparément le cas où $dureeExpiration vaut null qui indique que l’on veut une expiration à la fin de la session.
    • Le type de retour mixed nécessite la version 8 de PHP. En cas de problème, vous pouvez retirer les mixed.
  3. Codez la méthode
    public static function lire(string $cle): mixed
    
  4. Modifiez les actions deposerCookie et lireCookie pour utiliser la classe Cookie. Testez votre code, en particulier l’enregistrement d’une valeur qui n’est pas un string, et l’expiration des cookies.

  5. Codez la méthode
    public static function contient($cle) : bool
    

    Note : Un cookie existe si le tableau $_COOKIE contient une case à son nom. Vous pouvez tester ceci de deux manières équivalentes

    array_key_exists("nomCookie", $_COOKIE);
    isset($_COOKIE["nomCookie"]);
    

Enfin pour effacer un cookie,

  1. Codez et testez la méthode suivante de la classe Cookie :
    public static function supprimer($cle) : void
    
  2. Nettoyez le contrôleur utilisateur en commentant les actions deposerCookie et lireCookie.

Notes techniques

  1. La taille d’un cookie est limité à 4KB (car les en-têtes HTTP doivent être <4KB).

  2. Attention : la fonction setcookie() doit être appelée avant tout écriture de la page HTML. Le protocole HTTP impose cette restriction.

    Pourquoi ? Le Set-Cookie est une information envoyée dans l’en-tête de la réponse. Le corps de la réponse HTTP, c’est-à-dire la page HTML, doit être envoyée après son en-tête. Or PHP écrit et envoie la page HTML dans le corps de la réponse HTTP au fur et à mesure.

    Astuce : Une erreur classique est d’avoir un fichier PHP qui contient un espace ou un saut de ligne après la balise de fermeture PHP ?>. Cet espace indésirable peut faire dysfonctionner les cookies.
    Pour éviter ce problème, on évite de placer la balise de fermeture à la fin d’un fichier qui ne contient que du code PHP.

  3. C’est le navigateur qui stocke (ou pas) le cookie sur l’ordinateur du client. De manière générale, le serveur n’a aucune garantie sur le comportement du client : le cookie pourrait ne pas être enregistré (cookie désactivé chez l’utilisateur), ou être altéré (valeur modifiée, date d’expiration changée …).

    De même, c’est alors le navigateur du client qui se charge (normalement) de supprimer les cookies périmés chez le client. Encore une fois, le serveur n’a aucune garantie sur le comportement du client.

  4. Nous avons précédemment dit que le client envoie ses cookies à chaque requête HTTP. Mais heureusement le navigateur n’envoie pas tous ses cookies à tous les sites. Déjà, le nom de domaine du site est enregistré en même temps que les cookies pour se souvenir de leur provenance. Le comportement normal d’un navigateur est d’envoyer tous les cookies provenant des sous-domaines du domaine de la page Web qu’il demande.

    Par exemple, un cookie enregistré à l’initiative d’un site hébergé sur webinfo.iutmontp.univ-montp2.fr (nom de domaine univ-montp2.fr) sera disponible à tous les sites ayant ce nom de domaine, en particulier aux pages de *.univ-montp2.fr, mais pas aux autres domaines tels que google.fr.

    Il est possible de préciser ce comportement en donnant plus de paramètres à la fonction setcookie. On peut ainsi restreindre l’envoi des cookies à certains noms de domaine, à certains chemins (partie après le nom d’hôte dans l’URL), ou seulement aux URL utilisant le protocole sécurisé https.

Référence : La RFC des cookies

Exercice sur l’utilisation des cookies

Dans le site de covoiturage, vous avez défini que c’est le contrôleur voiture qui est affiché par défaut. Dans cet exercice, nous allons permettre à chaque visiteur du site de configurer son contrôleur par défaut.

Note importante : Cet exercice nécessite d’avoir codé plusieurs contrôleurs au TD précédent. Si ce n’est pas le cas, changez l’exercice pour personnaliser l’action par défaut plutôt que le contrôleur par défaut.

  1. Pour préparer la suite de l’exercice, nous allons mettre en place un contrôleur générique (si vous ne l’avez pas déjà fait au TD6). En effet, la future action de préférence de contrôleur par défaut n’est spécifique à aucun contrôleur en particulier. On va donc la rendre accessible à tous les contrôleurs.

    • Créez une classe src/Controleur/ControleurGenerique.php.
    • Les autres contrôleurs doivent hériter de ControleurGenerique.
    • Déplacez la méthode afficherVue commune à tous les contrôleurs dans ControleurGenerique. Sa visibilité passe de private à protected pour être accessible dans ses classes filles.
    • Si vous n’avez de contrôleur trajet src/Controleur/ControleurTrajet.php, créez un contrôleur vide qui hérite seulement du contrôleur générique.

    Note : Le contrôleur générique pourrait implémenter une méthode afficherErreur() générique. Cette méthode du contrôleur générique serait notamment appelée par le contrôleur frontal en cas de contrôleur inconnu.

  2. Dans votre menu qui se trouve dans l’en-tête commun de chaque page, ajouter une icône cliquable cœur qui pointe vers la future action afficherFormulairePreference (sans contrôleur).

    Note : Stockez vos images et votre CSS dans un dossier ressources accessible sur internet (avec le bon fichier .htaccess)

    assets

  3. Créez une action afficherFormulairePreference dans le contrôleur générique, qui doit afficher une vue src/vue/formulairePreference.php.

  4. Créez cette vue et complétez-la avec un formulaire
    • renvoyant vers la future action enregistrerPreference (sans indiquer de contrôleur),
    • contenant des boutons radio permettant de choisir voiture, trajet ou utilisateur comme contrôleur par défaut
      <input type="radio" id="voitureId" name="controleur_defaut" value="voiture">
      <label for="voitureId">Voiture</label>
      <input type="radio" id="utilisateurId" name="controleur_defaut" value="utilisateur">
      <label for="utilisateurId">Utilisateur</label>
      <input type="radio" id="trajetId" name="controleur_defaut" value="trajet">
      <label for="trajetId">Trajet</label>
      
  5. Afin de pouvoir gérer les préférences de contrôleur, créez une classe src/Lib/PreferenceControleur.php avec le bon espace de nom et le contenu suivant que vous complèterez
    class PreferenceControleur {
       private static string $clePreference = "preferenceControleur";
    
       public static function enregistrer(string $preference) : void
       {
          Cookie::enregistrer(PreferenceControleur::$clePreference, $preference);
       }
    
       public static function lire() : string
       {
          // À compléter
          return "";
       }
    
       public static function existe() : bool
       {
          // À compléter
       }
    
       public static function supprimer() : void
       {
          // À compléter
       }
    }
    
  6. Écrire l’action enregistrerPreference du contrôleur générique qui
    • récupère la valeur controleur_defaut du formulaire,
    • l’enregistre dans un cookie en utilisant la classe PreferenceControleur,
    • appelle une nouvelle vue src/vue/preferenceEnregistree.php qui affiche La préférence de contrôleur est enregistrée !.
  7. Vérifier que ce cookie a bien été déposé à l’aide des outils de développement.

  8. Dans le contrôleur frontal, le contrôleur par défaut est voiture. Faites en sorte d’utiliser la préférence de contrôleur par défaut si elle existe.

  9. Testez le bon fonctionnement de cette personnalisation de la page d’accueil en choisissant autre chose que voiture dans le formulaire.

  10. Il est possible que vos anciens liens du contrôleur voiture (vues liste et detail) et de la barre de menu (vueGenerale.php) n’indiquait pas le contrôleur voiture, car c’était le contrôleur par défaut. Si nécessaire, rajoutez l’indication du contrôleur dans ces liens.

  11. On souhaite que le formulaire de préférence soit déjà coché si la préférence existe déjà. Implémentez cette fonctionnalité. Vous utiliserez l’attribut checked pour cocher un <input type="radio">.

Les sessions

Les sessions sont un mécanisme basé sur les cookies qui permet de stocker des informations non plus du côté du client mais sur le serveur. Le principe des sessions est d’identifier les clients pour que le serveur puisse stocker des informations liées à chacun d’entre eux.

Pour faire ceci, la seule information stockée chez le client dans les cookies est un identifiant unique (par défaut dans la variable nommée PHPSESSID). Lorsqu’il demande une page, le client envoie son cookie contenant son identifiant (dans sa requête HTTP).

Le serveur stocke de son côté des informations liées à chaque client. En utilisant le cookie contenant l’identifiant, le serveur peut reconnaître quel client est en train de demander une page Web et ainsi récupérer les informations propres à ce client.

Schéma des sessions

Opérations sur les sessions

Présentons maintenant les opérations fondamentales sur les sessions :

Exercice sur les sessions

  1. Dans un nouveau fichier src/Modele/HTTP/Session.php, compléter la classe Session suivante
    namespace App\Covoiturage\Modele\HTTP;
        
    use Exception;
    
    class Session
    {
        private static ?Session $instance = null;
    
        /**
         * @throws Exception
         */
        private function __construct()
        {
            if (session_start() === false) {
                throw new Exception("La session n'a pas réussi à démarrer.");
            }
        }
    
        public static function getInstance(): Session
        {
            if (is_null(Session::$instance))
                Session::$instance = new Session();
            return Session::$instance;
        }
    
        public function contient($nom): bool
        {
            // À compléter
        }
    
        public function enregistrer(string $nom, mixed $valeur): void
        {
            // À compléter
        }
            
        public function lire(string $nom): mixed
        {
            // À compléter
        }
            
        public function supprimer($nom): void
        {
            // À compléter
        }
            
        public function detruire() : void
        {
            session_unset();     // unset $_SESSION variable for the run-time
            session_destroy();   // destroy session data in storage
            Cookie::supprimer(session_name()); // deletes the session cookie
            // Il faudra reconstruire la session au prochain appel de getInstance()
            $instance = null;
        }        
    }
    

    Note : Cette classe suit le patron de conception Singleton, car une session est forcément unique. De plus, on ne peut pas se satisfaire d’une classe statique comme Cookie, car une session a deux états : démarrée ou pas. Notre classe dynamique nous permets de nous assurer que la session est démarré avec session_start() avant de l’utiliser. En pratique, l’appel à une méthode dynamique comme enregistrer() nécessite d’avoir construit l’objet précédemment, donc d’avoir appelé session_start().

  2. Testez toutes les méthodes de Session dans une action temporaire du contrôleur utilisateur :
    1. Démarrer une session : observez le cookie de session avec les outils de développement,
    2. Écrire et lire des variables de session de différents types (chaînes de caractères, tableaux, objets, …),
    3. Supprimer une variable de session,
    4. Supprimer complètement une session avec notamment la suppression du cookie de session.

    Note : Voici un exemple d’utilisation de la classe Session

    $session = Session::getInstance();
    $session->enregistrer("utilisateur", "Cathy Penneflamme");
    

Vous appliquerez les sessions dans le prochain TD8 pour gérer l’authentification des utilisateurs. Nous vous proposons une autre application au TD9 avec les messages Flash.

Notes techniques

Avantages des sessions

Par rapport aux cookies, les sessions offrent plusieurs avantages. Il n’y a plus de limite de taille sur les données stockées côté client.

Mais surtout, l’utilisateur ne peut plus tricher en éditant lui-même le contenu du cookie. Imaginons par exemple que l’on note si l’utilisateur est administrateur dans la variable isAdmin d’un cookie. Alors rien n’empêche l’utilisateur de modifier son cookie en passant le champ isAdmin à la valeur true. Cependant, avec le mécanisme de sessions, l’utilisateur n’a pas accès aux données qui lui sont associées.

Expiration des sessions

  1. Durée de vie d’une session :

    Par défaut, une session est supprimée à la fermeture du navigateur. Ceci est dû au délai d’expiration du cookie de l’identifiant unique PHPSESSID qui est par défaut 0. Or nous avons vu dans la section sur les cookies que cela entraîne l’expiration du cookie à la fermeture du navigateur.

    Vous pouvez changer cela en modifiant la variable de configuration session.cookie_lifetime qui gère le délai d’expiration du cookie. La fonction session_set_cookie_params() permet de régler facilement cette variable.

    Ceci peut être utile si vous souhaitez que votre panier stocké avec des sessions soit conservé disons 30 minutes, même en cas de fermeture du navigateur.

  2. Comment rajouter un timeout sur les sessions :

    La durée de vie d’une session est liée à deux paramètres. D’une part, le délai d’expiration du cookie permet d’effacer l’identifiant unique côté client (sans garantie). D’autre part, une variable de PHP permet de définir un délai d’expiration aux fichiers de session (session.gc_maxlifetime) qui dira que le fichier peut être supprimé à partir d’un certain délai. Cependant, aucune de ces techniques n’offre de réelle garantie de suppression de la session après le délai imparti.

    La seule manière sûre de bien gérer la durée de vie d’une session est de stocker la date de dernière activité dans la session :

    if (isset($_SESSION['derniereActivite']) && (time() - $_SESSION['derniereActivite'] > ($dureeExpiration)))
        session_unset();     // unset $_SESSION variable for the run-time
    
    $_SESSION['derniereActivite'] = time(); // update last activity time stamp
    

    Référence : Stackoverflow

Rajoutez un mécanisme d’expiration pour les sessions. Le code du mécanisme sera codé dans une méthode verifierDerniereActivite de la classe Session. Cette méthode sera appelée par getInstance() après l’appel au constructeur pour ne vérifier l’expiration qu’au démarrage de la session.

Note : La durée d’expiration est une donnée qui dépend du site. Il serait donc judicieux de la mettre dans une classe de configuration ConfigurationSite.php (similaire à ConfigurationBaseDeDonnees.php).

Où sont stockées les sessions ?

Les sessions sont stockées sur le disque dur du serveur web, par exemple avec LAMP dans le dossier /var/lib/php/sessions (il faut être root pour y accéder). Reportez-vous à la partie session de la fonction phpinfo() pour connaître ce chemin.

Exemple : Si mon identifiant de session est PHPSESSID=aapot et si mon code PHP est le suivant

session_start();
$_SESSION['login'] = "rlebreton";
$_SESSION['isAdmin'] = "1";

alors le fichier /var/lib/php/sessions/sess_aapot contient

login|s:9:"rlebreton";isAdmin|s:1:"1";

(Optionnel) Au-delà des cookies et des sessions

Pour approfondir votre compréhension des cookies et des sessions, vous avez aussi accès aux notes complémentaires à ce sujet.

Cas d’utilisation classique : panier sur un site marchand

Alternatives aux cookies et sessions

Il existe des alternatives aux cookies et aux sessions. Par exemple, en utilisant l’API Web Storage de JavaScript (langage que nous étudierons au semestre 4).

Enfin, si l’on veut sécuriser des informations côté client (Cookie ou Web storage), une technologie répandue est le JSON Web Token (JWT).