Architecture d'un site web : manipulation des données

Youssef Chahir
GREYC — Université de Caen
TODO:
  • Gros problème : on ne peut pas décemment expliquer doctement que l'escape on input c'est pas bien, et utiliser directement PHP comme moteur de templates. L'échappement à la sortie n'est viable qu'avec un très bon système d'affichage qui échappe par défaut. Mais pour être utilisable il faut qu'il rende possible d'inclure facilement des bouts de HTML qui eux-même utilisent des variables et doivent donc les échapper, et là tout de suite c'est un autre projet.
  • La vue du site des couleurs est quand même assez dégueu.

Rappel :

Affichage tampon en php : Output buffering
  • Placer du contenu dans une mémoire tampon au lieu de l’afficher à l’écran.
  • Cela permet par exemple de traiter des informations complexes et de n’afficher le résultat que si les opérations se sont déroulées correctement.
  • Fonctions utiles :
    • ob_start() : fonction d’initialisation de l’output buffering
    • ob_get_contents() : récupération du contenu du buffer
    • ob_end_clean() : arrêt de l’opération d’output buffering et nettoyage du buffer
  • <?php
    ob_start
    ();
    echo 
    'Etape 2<br/>';
    echo 
    'Etape 3<br/>';
    $sortie=ob_get_contents();
    ob_end_clean();
    echo 
    'Etape 1<br/>';
    echo 
    $sortie;
    ?>
    Etape 1
    Etape 2
    Etape 3

Règle d'or

Tout ce qui vient du client est dangereux, il faut systématiquement s'en méfier.

Affichage

  • La règle d'or nous dit qu'il ne faut pas afficher n'importe comment les données qui viennent (potentiellement) du client.
  • L'internaute peut mettre du HTML (et donc du JavaScript) dans ses données : les afficher directement, c'est compromettre tous les utilisateurs !
  • Pour afficher des données qui viennent des clients dans une page HTML, il faut échapper les caractères spéciaux du HTML en les remplaçant par des entités HTML : < par &lt;, & par &amp;, etc.
  • La fonction PHP htmlspecialchars s'occupe de ça, mais il faut préciser certaines options :
    <?php
    $texte 
    "Du texte & de l'<abbr>HTML</abbr>.";
    $utf8inv 'Un caractère "'.hex2bin("C0").'" invalide.';
    echo 
    "1. ".htmlspecialchars($texte)."<br/>";
    echo 
    "2. ".htmlspecialchars($utf8inv)."<br/>";
    echo 
    "3. ".htmlspecialchars($texteENT_QUOTES ENT_SUBSTITUTE ENT_HTML5'UTF-8')."<br/>";
    echo 
    "4. ".htmlspecialchars($utf8invENT_QUOTES ENT_SUBSTITUTE ENT_HTML5'UTF-8')."<br/>";  
    ?>
    1. Du texte & de l'<abbr>HTML</abbr>.
    2.
    3. Du texte & de l'<abbr>HTML</abbr>.
    4. Un caractère "�" invalide.
  • ENT_QUOTES encode les apostrophes, ENT_SUBSTITUTE remplace les séquences UTF-8 invalides par le caractère � (U+FFFD) plutôt que de retourner une chaîne vide, et ENT_HTML5 utilise la table d'encodage d'HTML5.

Validation des données

  • La validation côté client est souvent utilisée pour améliorer l'expérience utilisateur, mais il est essentiel de se rappeler de la règle d'or : toujours valider les données côté serveur. La validation côté client peut être contournée ou désactivée, tandis que la validation côté serveur garantit l'intégrité des données.
  • Lorsqu'une validation côté serveur détecte des erreurs dans les données soumises par l'utilisateur, il est impératif de re-présenter le formulaire pré-rempli avec des indications claires sur ce qui doit être corrigé. Cela aide l'utilisateur à comprendre les problèmes et à les résoudre plus facilement.
  • La validation côté serveur devrait renvoyer un tableau d'erreurs contenant des messages explicites sur les problèmes rencontrés. Ce tableau d'erreurs peut ensuite être utilisé pour informer l'utilisateur des problèmes et l'orienter vers les corrections nécessaires.

Gestion du formulaire

  • L'affichage des éléments transmis ne se fait que si tous les champs ont été remplis.
  • En cas de problème lors de la soumission du formulaire, il est nécessaire de proposer à l'utilisateur le formulaire pré-rempli, en lui indiquant les éléments qui posent problème.
  • La validation du formulaire renvoie un tableau $erreurs contenant les messages d'erreur correspondants à chaque champ du formulaire. Ce tableau est utilisé pour informer l'utilisateur des problèmes rencontrés.
    • L'affichage des éléments manquants et des éléments déjà saisis se fait en accédant directement aux valeurs du formulaire via le script PHP, et en les gérant dans la page du formulaire.
    • De même, la gestion des éléments cochés (ou des boutons pressés) est traitée de la même manière en conservant l'état des cases cochées.
    • Pour cela, on utilise un tableau $coches qui mémorise l'état des boutons et/ou des cases à cocher. Par exemple, $coches["Nom_Bouton"] = "CHECKED"; $coches["Nom_Liste"] = "SELECTED";
    • Chaque message d'erreur est stocké dans le tableau $erreurs, où les clés correspondent aux champs du formulaire.
    • Le contrôle du formulaire commence par tester les erreurs possibles et afficher les messages d'erreur correspondants.
    • Ensuite, la page du formulaire est réaffichée avec les champs déjà remplis correctement (variables déjà saisies et boutons déjà cochés).

Démarche générale

La démarche générale consiste à accéder directement au formulaire via le script PHP (Formulaire.php), où les étapes suivantes sont effectuées :

  1. Si c'est la première fois que le formulaire est accédé, on inclut le formulaire HTML en vérifiant l'existence des variables correspondant aux noms des champs du formulaire dans $_GET ou $_POST :

    • if (!isset($_POST["nom"]) && !isset($_POST["prenom"])) { // ou if (!key_exists('nom', $_POST))

      include("form.html");

  2. Sinon, on récupère les données saisies via le tableau $_GET ou $_POST et on gère les éventuelles erreurs. Chaque message d'erreur est stocké dans un tableau $erreur, où les clés correspondent aux champs du formulaire. Les messages d'erreur sont ensuite affichés dans la page HTML du formulaire.

  3. On actualise le tableau $coche avec les cases déjà cochées et les options déjà sélectionnées :

    Si aucun choix n'a été fait, on ajoute un message d'erreur dans $erreur["choix"]. Sinon, on parcourt les choix faits et on met à jour $coche pour chaque case cochée.

    if (count($choix) == 0) {

    $erreur["choix"] = "vous n'avez pas fait de choix";

    }

    else {

    foreach ($choix as $case) { $coche[$case]="CHECKED"; }

    }

  4. On compte le nombre d'erreurs (nombre d'éléments dans le tableau $erreur). S'il y a des erreurs, on recharge le formulaire. Sinon, toutes les données sont correctes et le script peut appeler d'autres fonctions d'affichage ou de traitement des données :

    if (count($erreur) == 0) { include("Traitement.php"); }

    else { include("Formulaire.html"); }

Exemple

Page PHP : form.php

<?php
require("libF.php");

if (!isset(
$_POST["nom"]) && !isset($_POST["prenom"])) {
    include(
"form.html");
    echo 
$formulaire;
} else {
    
$nom trim($_POST['nom']) ?? null;
    
$prenom trim($$_POST['prenom']) ?? null;
    
$dateN trim($_POST['dateN']) ?? null;
    
$lieuN trim($_POST['lieuN']) ?? null;
    
$codeP trim($_POST['codeP']) ?? null;
    
$telephone trim($_POST['telephone']) ?? null;
    
$email trim($_POST['email']) ?? null;
    
$ville trim($_POST['ville']) ?? null;
    
$civilite $_POST['civilite'] ?? null;
    
$comment trim($_POST['comment']) ?? null;
    
/*
     $nom = key_exists('nom', $_POST)? trim($_POST['nom']): null;
     $prenom = key_exists('prenom', $_POST)? trim($_POST['prenom']): null;
     $dateN = key_exists('dateN', $_POST)? trim($_POST['dateN']): null;
     $lieuN = key_exists('lieuN', $_POST)? trim($_POST['lieuN']): null;
     $codeP = key_exists('codeP', $_POST)? trim($_POST['codeP']): null;
     $telephone = key_exists('telephone', $_POST)? trim($_POST['telephone']): null;
     $email = key_exists('email', $_POST)? trim($_POST['email']): null;
     $ville = key_exists('ville', $_POST)? $_POST['ville']: null;
     $civilite = key_exists('civilite', $_POST)? $_POST['civilite']: null;
     $comment = key_exists('comment', $_POST)? trim($_POST['comment']): null;
     */

    
$erreur = [];
    
$coche = [];

    if (empty(
$nom)) {
        
$erreur["nom"] = "il manque un nom";
    } elseif (
is_numeric($nom)) {
        
$erreur["nom"] = "nom ne doit pas être un nombre";
    } elseif (
is_numeric($nom[0])) {
        
$erreur["nom"] = "nom ne doit pas commencer par un chiffre";
    }

    if (empty(
$prenom)) $erreur["prenom"] = "il manque un prenom";
    if (empty(
$lieuN)) $erreur["lieuN"] = "il manque un lieuN";
    if (empty(
$codeP)) {
        
$erreur["codeP"] = "il manque un codeP";
    } elseif (!
controlerCP($codeP)) {
        
$erreur["codeP"] = "codeP incorrect";
    }

    if (empty(
$dateN)) {
        
$erreur["dateN"] = "il manque une dateN";
    } elseif (!
controlerDate($dateN)) {
        
$erreur["dateN"] = "dateN incorrect";
    }

    if (empty(
$email)) {
        
$erreur["email"] = "il manque un email";
    } elseif (!
controlerEmail($email)) {
        
$erreur["email"] = "email incorrect";
    }

    if (empty(
$telephone)) {
        
$erreur["telephone"] = "il manque un telephone";
    } elseif (!
controlerTel($telephone)) {
        
$erreur["telephone"] = "telephone incorrect";
    }

    if (
$ville == "Choisir"$erreur["ville"] = "il manque une ville";
    if (empty(
$comment)) $erreur["comment"] = "il manque un commentaire";
    if (empty(
$civilite)) $erreur["civilite"] = "Préciser H ou F";

    
$coche[$civilite] = "CHECKED";
    
$coche[$ville] = "SELECTED";

    
$compteur_erreur count(array_filter($erreur));

    if (
$compteur_erreur == 0) {
        
$_POST["comment"] = nl2br($comment);
        
$corps "<h1>Données du formulaire </h1>";

        foreach (
$_POST as $cle => $val) {
            if (
$cle != "user_valider") {
                
$corps .= "<b>" $cle "</b>:  \t\t\t" $val "<br>";
            }
        }
        
$corps .= "<br>";
        echo 
$corps;
    } else {
        include(
"form.html");
        echo 
$formulaire;
    }
}
?>

Squelette : form.html


<!DOCTYPE html>
<html lang="fr">
<head>
    <meta charset="UTF-8">
    <title>Formulaire</title>
    <style>
        .rTable { display: table; }
        .rTableRow { display: table-row; }
        .rTableHeading { display: table-header-group; }
        .rTableBody { display: table-row-group; }
        .rTableFoot { display: table-footer-group; }
        .rTableCell, .rTableHead { display: table-cell; }
        .blink { color: red; animation: blink-animation 1s steps(5, start) infinite; }
        @keyframes blink-animation { to { visibility: hidden; } }
    </style>
    <link rel="stylesheet" href="https://www.w3schools.com/w3css/4/w3.css">
</head>


<body>
  <?php
$formulaire
=<<<EOT
  <form method="post" action="form.php" name="form_user" >
  <table>
    <tr>
      <td><label> Nom</label></td> 
      <td><input type="text" name="nom" size="" value="
{$nom}"></td>
      <td class="w3-text-red">
{$erreur["nom"]}</td>
    </tr>
    <tr>
      <td><label> Prénom</label></td>
      <td><input type="text" name="prenom" size="" value="
{$prenom}"></td>
      <td class="w3-text-red">
{$erreur["prenom"]}</td>
    </tr>
    <tr>
      <td><label> Date de Naissance</label></td>
      <td><input type="text" name="dateN" size=""  placeholder="jj-mm-aaaa" value="
{$dateN}"></td>
      <td class="w3-text-red">
{$erreur["dateN"]}</td>
    </tr>
    <tr>
      <td><label> Lieu de Naissance</label></td>
      <td><input type="text" name="lieuN" size="" value="
{$lieuN}"></td>
      <td class="w3-text-red">
{$erreur["lieuN"]}</td>
    </tr>
    <tr>
      <td><label> Code Postal</label></td>
      <td><input type="text" name="codeP" size="" value="
{$codeP}"></td>
      <td class="w3-text-red">
{$erreur["codeP"]}</td>
    </tr>
    <tr>
      <td><label> Téléphone</label></td>
      <td><input type="text" name="telephone" size="" value="
{$telephone}"></td>
      <td class="w3-text-red">
{$erreur["telephone"]}</td>
    </tr>
    <tr>
      <td><label> Email</label></td>
      <td><input type="text" name="email" size="" value="
{$email}"></td>
      <td class="w3-text-red">
{$erreur["email"]}</td>
    </tr>
    <tr>
      <td><label> Ville</label></td>
      <td><select name="ville" size="1" >
        <option value="Choisir" 
{$coche["Choisir"]} >Choisir une ville</option>
        <option value="Amiens" 
{$coche["Amiens"]} >Amiens</option>
        <option value="Paris" 
{$coche["Paris"]} >Paris</option>
        <option value="Lyon" 
{$coche["Lyon"]} >Lyon</option>
        </select>
      </td>
      <td class="w3-text-red">
{$erreur["ville"]}</td>
    </tr>
    <tr>
      <td><label> Civilité</label></td>
      <td>    
        <input type="radio" name="civilite" value="homme" 
{$coche["homme"]}/>Homme
        <input type="radio" name="civilite" value="femme" 
{$coche["femme"]}/>Femme
      </td>
      <td class="blink">
{$erreur["civilite"]}</td>
    </tr>
    <tr>
      <td><label> Commentaire</label></td>
      <td><textarea name="comment" cols='40' rows='3' >
{$comment} </textarea></td>
      <td class="w3-text-red">
{$erreur["comment"]}</td>
    </tr>
    <tr>
      <td><input type="submit" name="user_valider" value="Valider" size="" ></td>
      <td></td>
      <td></td>
    </tr>
  </table>
</form>
EOT;
?>
</body>
</html>


Résultat

CRUD

  • CREATE : création des éléments dans la base
  • READ : lecture d'un ou plusieurs enregistrements
  • UPDATE : mettre à jour un enregistrement
  • DELETE : supprimer un enregistrement

Échappement à l'entrée

Entité HTML brodée sur un T-shirt
Échappement à l'entrée : échec. (Source)
  • Attention, la façon d'échapper la sortie dépend de ce qu'on affiche.
  • Par ex., pour une sortie en mode texte, on ne veut pas échapper les caractères spéciaux.
  • ⇒ il ne faut pas échapper les données en entrée, (et les stocker par exemple dans une base de données) : on ne sait pas ce qu'on voudra en faire plus tard.
  • Un article sur le sujet