Grégory Bourguin
SysReIC - LISIC - ULCO
PHP (Formulaires et Données)

Recevoir des Données

Introduction

Comme nous l'avons vu dans le cours de Javascript sur les requêtes et les formulaires,
il est possible pour un client (navigateur) d'envoyer des données au serveur Web.

Nous avons aussi vu qu'il existe 2 méthodes pour transmettre ces données :

  • Méthode GET : les données sont transmises au serveur en étant ajoutées à URL demandée.
  • Méthode POST : les données sont encapsulées dans la requête HTPP.

Le serveur Web transmet ces données au script PHP visé dans des variables dites "superglobales", de type array, et nommées $_GET et $_POST.

NB: il existe en PHP de nombreuses superglobales autres que $_GET et $_POST et qui servent des buts divers : nous rencontrerons certaines d'entre elles dans la suite de ce cours.

La Méthode GET

Rappels

Avec la méthode GET, les données sont transmises au serveur en étant ajoutées à URL demandée : par conséquent, la taille de ces données ne peut excéder 2048 caractères.

La syntaxe d'une requête HTTP GET est : url?name=value[&name=value].
(name est le nom du paramètre transmis, et value sa valeur)

Ce type de requête peut être transmise au serveur par différents moyens :

  • Dans un lien avec la balise a.
  • Dans l'attribut action d'un formulaire en spécifiant la method='GET'.
  • Avec une requête de type AJAX.

Superglobale $_GET

$_GET est un tableau associatif généré à partir des paramètres de la requête.

On accède à la value de chaque paramètre grâce à la clé name dans $_GET.

NB: on peut aussi accéder à ces données par la superglobale $_REQUEST qui regroupe les données $_GET et $_POST.

Un exemple très simple

Soit un script PHP nommé 01_length_get.php dont le but est de recevoir un message sous forme de chaine et, en réponse, de l'afficher en indiquant sa longueur.

On peut accéder à ce script via l'URL :
http://web.gregory-bourguin.fr/teaching/php/requests/samples/01_length_get.php

Résultat.

La chaine est vide et la longueur est 0 puisque nous n'avons pas transmis de paramètre.


Le code de ce script est le suivant :
01_get_length.php
<?php
// récupération du paramètre
$param = $_GET['msg'] ;

// encodage avant affichage pour éviter les failles...
$chaine = htmlspecialchars($param) ;

// calcul de la longueur
$longueur = strlen($chaine) ;
?>
La chaine reçue par GET est : "<b><?php echo $chaine ?></b>".
<br>
Sa longueur est de <b><?php echo $longueur ?></b> caractère(s).

On peut voir dans ce code source que le script attend un paramètre GET nommé msg.

Avec cette information, on peut formuler un appel correct avec par exemple :
http://web.gregory-bourguin.fr/teaching/php/requests/samples/01_length_get.php?msg=Hellooo

Résultat.

Avec un formulaire

Ce script peut être utilisé en tant qu'action d'un form utilisant la méthode GET.

Lors de la soumission d'un formulaire, ce sont les champs du formulaire qui sont transformés en paramètres de la requête sous forme de paires name=value, name étant le nom du champ éponyme, et value sa valeur au moment du submit.


On peut par exemple créer le formulaire :
<!--Bootstrap-->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/js/bootstrap.bundle.min.js" integrity="sha384-ygbV9kiqUc6oa4msXn9868pTtWMgiQaeYH7/t7LECLbyPA2x65Kgf80OJFdroafW" crossorigin="anonymous"></script>

<!--Le formulaire-->
<form method="get"
      action="http://web.gregory-bourguin.fr/teaching/php/requests/samples/01_length_get.php"
      target="_blank"
      style="width: 30%; margin: 10px">
    <input type="text" class="form-control" id="message" placeholder="Votre message..."
           name="msg">
    <button type="submit" class="btn btn-primary" style="margin-top: 10px ; width: 100%">
        Envoyer
    </button>
</form>
Résultat.

La Méthode POST

Rappels

Avec POST, les données sont transmises en étant encapsulées dans la requête HTTP :

  • la taille de ces données n'est pas limitée.
  • les paramètres n'apparaissent pas dans l'URL.

Le fait que les paramètres d'une requête POST n'apparaissent pas dans l'URL ne signifie pas qu'ils sont invisibles : les données transitent en clair sur le réseau!
Pour qu'une requête POST soit sécurisée, il faut utiliser le protocole https.


Ce type de requête peut être transmise au serveur par différents moyens :

  • Dans l'attribut action d'un formulaire en spécifiant la method='POST'.
  • Avec une requête de type AJAX.

Superglobale $_POST

$_POST est un tableau associatif généré à partir des paramètres de la requête.

On accède à la value de chaque paramètre grâce à la clé name dans $_POST.

NB: on peut aussi accéder à ces données par la superglobale $_REQUEST qui regroupe les données $_GET et $_POST

Un exemple très simple

Soit un script PHP nommé 02_length_post.php dont le but est de recevoir un message sous forme de chaine et, en réponse, de l'afficher en indiquant sa longueur.

On peut accéder à ce script via l'URL :
http://web.gregory-bourguin.fr/teaching/php/requests/samples/02_length_post.php

Résultat.

Le résultat est 0, ce qui est normal puisque nous n'avons pas transmis de paramètre.


Le code de ce script est le suivant :
02_length_post.php
<?php
// récupération du paramètre
$param = $_POST['msg'] ;

// encodage avant affichage pour éviter les failles...
$chaine = htmlspecialchars($param) ;

// calcul de la longueur
$longueur = strlen($chaine) ;
?>
La chaine reçue par POST est : "<b><?php echo $chaine ?></b>".
<br>
Sa longueur est de <b><?php echo $longueur ?></b> caractère(s).

On peut voir dans ce code source que le script attend un paramètre POST nommé msg.


Cependant, cette fois-ci, ON NE PEUT PAS formuler un appel correct avec :
http://web.gregory-bourguin.fr/teaching/php/requests/samples/02_length_post.php?msg=Hellooo

La chaine sera toujours vide et la longueur 0 :
POST NE PEUT PAS recevoir ses paramètres via l'URL !

Avec un formulaire

Ce script peut être utilisé en tant qu'action d'un form utilisant la méthode POST.

Lors de la soumission d'un formulaire, ce sont les champs du formulaire qui sont transformés en paramètres de la requête sous forme de paires name=value, name étant le nom du champ éponyme, et value sa valeur au moment du submit.


On peut par exemple créer le formulaire (method='POST'):
<!--Bootstrap-->
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-giJF6kkoqNQ00vy+HMDP7azOuL0xtbfIcaT9wjKHr8RbDVddVHyTfAAsrekwKmP1" crossorigin="anonymous">
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta1/dist/js/bootstrap.bundle.min.js" integrity="sha384-ygbV9kiqUc6oa4msXn9868pTtWMgiQaeYH7/t7LECLbyPA2x65Kgf80OJFdroafW" crossorigin="anonymous"></script>

<!--Le formulaire-->
<form method="post"
      action="http://web.gregory-bourguin.fr/teaching/php/requests/samples/02_length_post.php"
      target="_blank"
      style="width: 30%; margin: 10px">
    <input type="text" class="form-control" id="message" placeholder="Votre message..."
           name="msg">
    <button type="submit" class="btn btn-primary" style="margin-top: 10px ; width: 100%">
        Envoyer
    </button>
</form>
Résultat.

Vérification des Données

Quelle que soit la méthode de réception des données (GET ou POST), il est très important de ne jamais faire confiance à ce qu'un script va recevoir du réseau.

En effet, même si vous créer un formulaire très "directif", on ne peut jamais être certain que l'utilisateur va le remplir correctement.

Comme nous l'avons vu lors de l'études du traitement des formulaires en Javascript , il est possible de procéder à diverses vérifications sur le poste du client avant envoi des données au serveur. Cependant, il est tout à fait possible qu'un client désactive Javascript sur son navigateur, passant ainsi outre les vérifications.

Rien n'empêche quelqu'un de créer une URL "à la main" (pour un GET) avec les paramètres qu'il désire.

On peut utiliser les outils de développement du navigateur Web pour modifier un formulaire.

Il existe de nombreux outils dont le but est spécifiquement de créer des requêtes.

Il ne faut pas oublier que les formulaires servent souvent à récupérer des données de l'utilisateur pour les ré-utiliser ultérieurement dans votre site Web. En d'autres termes, les données reçues vont souvent finir faire partie de votre site (par exemple en les enregistrant dans votre BD, puis en les réutilisant dans une page générée en PHP).

Sans vérifications/protections, un utilisateur pourrait introduire des informations qui au mieux feront afficher n'importe quoi à vos pages, et au pire seront des instructions injectées (du code) dans le but de planter votre site ou encore de subtiliser des données confidentielles.

Valeur

Est-ce que le paramètre existe ?

La première chose à faire est de vérifier que la donnée a bien été transmise.

Prenons comme exemple le script 01_length_get.php que nous avons vu ci-avant.

Lorsqu'on appelle ce script sans lui transmettre de paramètre, voici ce qui se passe :

Vous pouvez constater que le script indique que la chaine reçue est "", et que sa longueur est 0... ce qui est faux : la chaine n'est pas vide, elle n'a en réalité pas été reçue !

Fonction isset

PHP fournit la fonction isset qui permet de déterminer si une variable existe.

Nous avons vu que les paramètres d'une requête sont automatiquement "transformés" en variables dans les superglobales $_GET et $_POST. Lorsque le paramètre n'a pas été transmis, ces variables n'existent pas et isset renverra false.

On peut alors transformer notre script PHP comme suit :
01_length_get_verif.php (avec isset)
<?php

if (isset($_GET['msg'])){ // si le paramètre msg a bien été reçu...
    // récupération du paramètre
    $param = $_GET['msg'] ;

    // encodage avant affichage pour éviter les failles...
    $chaine = htmlspecialchars($param) ;

    // calcul de la longueur
    $longueur = strlen($chaine) ;
?>
    La chaine reçue par GET est : "<b><?php echo $chaine ?></b>".
    <br>
    Sa longueur est de <b><?php echo $longueur ?></b> caractère(s).
<?php
} else { // si le paramètre msg n'a pas été reçu...
    echo "<b style='color: red'>Je n'ai reçu aucun message !</b>" ;
}
?>

Voici maintenant ce qui se passe si on appelle le script sans paramètre msg :
Et voici, un appel avec un paramètre msg contenant une chaine vide :

Ce qui est tout de même plus cohérent...

Formulaires et la fonction empty

Nous avons ici pris l'exemple avec GET, mais isset permet aussi de tester les paramètres $_POST.

Il faut cependant savoir que lors de la soumission d'un formulaire, si un champ de type text n'a pas été rempli, PHP reçoit (tout de même) la variable correspondant au name de l'input avec la valeur ''.

Dans le cas d'un submit avec un formulaire, isset permet donc de savoir si le formulaire qui a appelé le script possédait le champ (ici un input avec name='msg'), mais isset ne peut pas servir à savoir si l'utilisateur a "oublié" de remplir ce champ. Dans ce cas, la variable existe mais elle est vide.


La fonction empty vérifie si une variable n'existe pas, ou si elle est vide.

On peut alors écrire une vérification de formulaire telle que :
02_length_post_verif.php (avec empty)
<?php
if (!empty($_POST['msg'])){
    // récupération du paramètre
    $param = $_POST['msg'] ;

    // encodage avant affichage pour éviter les failles...
    $chaine = htmlspecialchars($param) ;

    // calcul de la longueur
    $longueur = strlen($chaine) ;
    ?>
    La chaine reçue par POST est : "<b><?php echo $chaine ?></b>".
    <br>
    Sa longueur est de <b><?php echo $longueur ?></b> caractère(s).
<?php
} else {
    echo "<b style='color: red'>Je n'ai reçu aucun message !</b>" ;
}
?>

Ce qui aura pour effet :
Résultat.
(cliquez sur 'Envoyer' sans remplir le champ pour voir l'effet de empty)

Type & Plage de Valeurs

Pour terminer, il faut aussi considérer la concordance des types et les (plages de) valeurs reçues par un script PHP pour vérifier qu'elles n'engendrent pas de problème.

On peut par exemple imaginer un formulaire qui permet de lancer une recherche en spécifiant le nombre de produits maximum à afficher sur la page réponse dans un champ nommé max.

Il est possible que le script PHP reçoive un paramètre max contenant une chaine qui ne peut pas être interprétée comme un nombre. Une solution peut être de le vérifier avec une fonction telle que is_numeric, puis d'effectuer un cast avant de traiter la donnée.

De plus, même si vous avez mis un range slider qui limite la plage de valeurs possibles, rien n'empêche une requête "bidouillée" d'arriver avec une valeur trop grande pour l'application.

Dans tous les cas, la solution est d'introduire des tests dans le traitement PHP pour faire en sorte que, quelles que soient les valeurs des paramètres reçues, tout se passe bien.

Format

Il n'est pas rare que les données reçues ne respecte pas le format que vous attendiez.

Même sans mauvaise intention, les informations peuvent être formatées différemment :

  • Un téléphone : (+33)..., 33..., 0... ???
  • Une date : jj/mm/aaaa, jj/mm/aa, jjjj mmmm aaaa ???
  • etc.

PHP founit divers outils qui permettent de vérifier le format des données.

Expressions régulières et PRCE

PRCE fournit des fonctions permettant de travailler avec des expressions régulières.

Un expression connue est par exemple celle pour vérifier le format d'une adresse mail :

<?php
function checkMailPattern($mail){
    $pattern = '/^[^\W][a-zA-Z0-9\-\._]+[^\W]@[^\W][a-zA-Z0-9\-\._]+[^\W]\.[a-zA-Z]{2,4}$/' ;
    if (preg_match($pattern, $mail)){
        echo "<p style='color: blue'><b>$mail</b> a un format d'adresse valide</p>" ;
    }else{
        echo "<p style='color: red'><b>$mail</b> n'a pas un format d'adresse valide</p>" ;
    }
}

$mail1 = "gregory.bourguin@univ-littoral.fr" ;
checkMailPattern($mail1);

$mail2 = "n'importe quoi" ;
checkMailPattern($mail2);
Résultat

gregory.bourguin@univ-littoral.fr a un format d'adresse valide

n'importe quoi n'a pas un format d'adresse valide

Cependant, si PRCE permet de traiter des expressions régulières complexes, PHP offre d'autres moyens pour les patterns "classiques" comme les emails...

La fonction filter_var

La fonction PHP filter_var permet de tester avec des filtres prédéfinis.

Ainsi, la vérification de format d'email pourra être faite avec :

<?php
function checkMailPattern2($mail)
{
    if (filter_var($mail, FILTER_VALIDATE_EMAIL)) {
        echo "<p style='color: blue'><b>$mail</b> a un format d'adresse valide</p>" ;
    }else{
        echo "<p style='color: red'><b>$mail</b> n'a pas un format d'adresse valide</p>" ;
    }
}

$mail1 = "gregory.bourguin@univ-littoral.fr";
checkMailPattern2($mail1);

$mail2 = "n'importe quoi";
checkMailPattern2($mail2);
Résultat

gregory.bourguin@univ-littoral.fr a un format d'adresse valide

n'importe quoi n'a pas un format d'adresse valide

NB: le fait que le format soit valide ne signifie pas que l'adresse fonctionne...

Faille XSS

Le problème : injection de code

Pour comprendre le problème de la faille XSS, reprenons l'exemple de notre formulaire de comptage de caractères qui envoie un champ nommé msg en paramètre de la requête.

Un script naïf qui traite ce formulaire pourrait être le suivant :
(Cet exemple est en POST mais ce serait pareil avec GET)
script_naif.php
<?php
// récupération du paramètre msg
$chaine = $_POST['msg'] ;

// calcul de la longueur
$longueur = strlen($chaine) ;
?>
La chaine reçue par POST est : "<b><?php echo $chaine ?></b>".
<br>
Sa longueur est de <b><?php echo $longueur ?></b> caractère(s).

Comme on peut le voir, le script récupère le message entré part l'utilisateur et envoie une réponse qui l'affiche tel quel dans une page Web.

Que se passe-t-il si l'utilisateur n'entre pas un simple texte, mais du code HTML ?

Avec ce script, si l'utilisateur remplit le champ avec par exemple :
<script>alert('I am the Best')</script>

...le script PHP va insérer ce code HTML dans la page réponse et, par conséquent,
le code Javascript va être déclenché dès son chargement !

Alors... dans cet exemple, ça peut ne pas paraitre trop grave puisque si l'utilisateur entre ce code HTML, c'est sa propre page réponse qui sera affectée.

Par contre, il arrive souvent dans un site web que les données reçues d'un utilisateur soient stockées dans le site pour être affichées à d'autres utilisateurs.

Imaginez que ce code soit dans un commentaire posté par un utilisateur sur un site de e-commerce : toutes les personnes qui affichent la page du produit concernée (avec les commentaires) auront "la joie" de voir apparaitre l'alerte...

... et bien entendu, un utilisateur aussi féru de Javascript que vous pourrait injecter du code bien plus nuisible qu'une "simple" alerte Javascript (récupération de données de navigation, etc.).

La solution : htmlspecialchars

Heureusement, PHP fournit des moyens permettant de palier ce type de problèmes.

La fonction PHP htmlspecialchars convertit les caractères spéciaux en entités HTML.

Ainsi, par exemple, les caractères < et > seront transformés en &lt; et &gt; : ils ne seront donc plus interprétés par le navigateur comme des ouvertures/fermetures de balises !

Vous aviez d'ailleurs certainement remarqué que le script de traitement de notre exemple qui vous avait été donnée au début du cours utilisait (déjà) htmlspecialchars :
02_length_post.php
<?php
// récupération du paramètre
$param = $_POST['msg'] ;

// encodage avant affichage pour éviter les failles...
$chaine = htmlspecialchars($param) ;

// calcul de la longueur
$longueur = strlen($chaine) ;
?>
La chaine reçue par POST est : "<b><?php echo $chaine ?></b>".
<br>
Sa longueur est de <b><?php echo $longueur ?></b> caractère(s).

Avec cette méthode, si un utilisateur tente d'injecter du code dans le formulaire, ce code ne sera pas exécuté par un navigateur car il est encodé par htmlspecialchars avant d'être utilisé dans une page réponse :

Un formulaire pour entrer un message.
(vous pouvez tenter d'injecter du code ;) )

TP 02

Le but de ce TP est de créer une interface de login avec un formulaire "classique" d'authentification.

Votre objectif est de créer une page (principale) en php dont le contenu va changer en fonction de l'état de connexion de l'utilisateur.

NB: puisque nous n'avons pas encore vu la notion de session, la gestion de l'état de connexion sera ici très rudimentaire.

Pour vous mettre dans l'ambiance, voici le site à réaliser :

Indice : stark / warmachinerox
(cf. instructions ci-après)

Fichiers principaux

Le site contient principalement un fichier index.php, incluant lui-même un header.php et un footer.php.

Lors de son 1er affichage, la page présente un formulaire d'authentification (on utilisera la méthode POST pour ne pas avoir le mot de passe qui se retrouve dans la barre d'adresse...). Ce formulaire contient 2 champs nommés username et password.

La soumission du formulaire ré-affiche index.php en utilisant les entrées fournies par l'utilisateur (lors de la soumission précédente) pour mettre à jour la page :

  • Si le formulaire est mal rempli, un message d'erreur est généré, et le formulaire est ré-affiché. NB: normalement, on ferait (aussi) une vérification en Javascript, mais on s'en passera ici puisque le but est de s'entrainer à générer du contenu en PHP.
  • Si l'authentification est réussie, la page affiche simplement un message de bienvenue, et un bouton "LOGOUT" apparait.

La page est construite avec un header.php et un footer.php. Ces fichiers sont assez simples : ils contiennent respectivement un header et un footer :). Cependant, vous noterez que c'est dans le header qu'apparait le bouton "LOGOUT" quand l'utilisateur a entré les bonnes informations.

Vous aurez aussi à créer d'autres fichiers pour les CSS, JS et la classe impliqués.

Classe jarvis\JarvisLogger

Pour la gestion de l'authentification, vous créerez une classe jarvis\JarvisLogger.

La classe jarvis\JarvisLogger possède les méthodes suivantes :

  • public function generateLoginForm(string $action): void
    Permet de générer le code HTML du formulaire d'authentification (méthode POST). Le paramètre action contient l'adresse du script php qui va traiter le formulaire (ici ce sera donc index.php).
  • public function log(string $username, string $password) : array
    Cette méthode vérifie si l'authentification est correcte, c.a.d. si le $username et le $password passés en paramètre correspondent.
    Dans un cas réel, la vérification se ferait en interrogeant une base de données. Mais comme nous n'avons pas encore abordé ce sujet, on mettra exceptionnellement ici les informations "en dur" dans le code.
    Le $username attendu sera "stark".
    Le $password attendu sera "warmachinerox".
    La méthode retourne un tableau associatif contenant 3 paires clé/valeur :
    • 'granted' : un bool indiquant si l'authentification est réussie.
    • 'nick' : le nickname de l'utilisateur s'il a été authetifié (ici ce sera "tony"), ou null sinon.
    • 'error' : contient null si l'authentification est réussie, "username is empty" si $username est vide, "password is empty" si le $password est vide, ou "authentication failed si les informations fournies ne sont pas vides mais ne correspondent pas à ce qui était attendu.

La classe jarvis\JarvisLogger est utilisée dans index.php pour :

  • générer le formulaire
  • tester les données d'authentification lorsque index.php est exécuté suite à un submit. La réponse est alors utilisée pour adapter la page (afficher un message de bienvenue, un message d'erreur, etc.)

Ressources

Vous aurez besoin des ressources suivantes :
  • Le fichier image jarvis.jpg.
  • La google font qui peut être importée avec :
    @import url('https://fonts.googleapis.com/css2?family=Unica+One&display=swap');
    et utilisée avec : font-family: 'Unica One', cursive;

Remarque Importante

Si les mécanismes présentés ici sont fonctionnels, dans une situation réelle, il faudrait aussi renforcer la sécurité en mettant en place un protocole sécurisé (https) !

La correction se trouve ICI

TP 03

Le but de ce TP est de créer un mini jeu en PHP dans lequel l'utilisateur doit deviner un mot secret.

Fichiers principaux

Le site contient principalement un fichier index.php qui affiche un indice (les lettres "cachées" du mot à découvrir), et propose un formulaire qui permet de faire une proposition de mot.

La soumission d'une proposition (donc du formulaire) ré-affiche index.php en utilisant les entrées fournies par l'utilisateur pour mettre à jour l'indice, c.a.d. lui montrer les lettres qu'il a découvertes. Sa dernière proposition est aussi reprise dans le champ (pour lui rappeler ce qu'il vient de proposer).

La page est construite avec un header.php et un footer.php. Le header présente un nav qui contient 2 liens : l'un vers index.php, l'autre vers rules.php qui affiche les "règles" du jeu. Vous remarquerez que le header et le footer sont inclus dans les 2 pages.

Vous aurez aussi à créer d'autres fichiers pour les CSS, JS et classe(s) impliqués.

Classe swg\SecretWordGame

Pour le traitement du jeu, vous créerez une classe nommée swg\SecretWordGame.

La classe swg\SecretWordGame possède (entre autres) les méthodes suivantes :

  • public function __construct(string $secret)
    Un constructeur d'instance du jeu initialisée avec le mot secret passé en paramètre. Même si c'est une string qui est passé en paramètre, il vous est conseillé de stocker le secret dans une variable d'instance sous la forme d'un array plutôt que string (en prévision de la version finale du jeu).
    (NB: voir str_split pour splitter, et join au besoin pour l'effet inverse).
  • public function try(?array $word=null): array
    Teste le tableau word passé en paramètre pour évaluer cette proposition.
    La méthode renvoie une réponse sous forme de tableau associatif contenant 3 paires clé/valeur :
    • 'word' : un array rappelant la proposition.
    • 'win' : un bool indiquant si la partie est gagnée.
    • 'result' : une string qui représente l'avancée du joueur, c.a.d les lettres du mot secret qui ont été découvertes, et des ? à la place de celles qui ne l'ont pas été. (ex. secret='moi', word='ici' -> result='??i').
  • public function generateInput(?array $response): void
    La méthode prend en paramètre une réponse (précédemment générée par un try) et génère le code HTML correspondant à afficher à l'utilisateur, i.e. le result + le formulaire pour la prochaine proposition (pré-rempli avec le dernier mot entré).
  • public function generateWin() : void
    Génère le code HTML pour indiquer que l'utilisateur a gagné, affichant le mot secret et le message flashy "!!! YOU WIN !!!".

Version 1 : --> CLIQUEZ ICI <--

Dans la version 1 du jeu, generateInput affiche "siplement" le result et un champ unique de type text pour les propositions.

L'envoie de la proposition peut être fait par un appui sur la touche "entrée", ou avec le bouton "Try".

Version 2 : --> CLIQUEZ ICI <--

Ici generateInput génère 1 champ de type text par lettre cachée du mot secret.

On utilise du Javascript pour que :

  • la 1ère lettre non découverte ait automatiquement le focus lors du chargement.
  • l'utilisateur ne puisse entrer qu'une seule lettre par champ : chaque lettre entrée fait passer au champ suivant (NB : pour le dernier champ disponible, le contenu est remplacé et le curseur reste dans la case...).
  • l'envoie de la proposition puisse être fait par un appui sur la touche "entrée", ou avec le bouton "Try".

Vous remarquerez que lorsqu'une lettre est découverte, le champ correspondant la conserve mais est désactivé.

NB: les champs sont générés en fonction du mot secret passé au constructeur !
.. en d'autres termes, la liste des input n'est pas "en dur".

Quelques remarques importantes :
  • Il est possible de nommer les input (attribut name) avec des noms de cases d'un tableau. Par exemple, un <input name=letter[0] ...> avec une méthode GET sera accessible en PHP dans la variable $_GET['letter'][0].
  • Il faut savoir que lorsqu'un input est désactivé (disabled), PHP ne reçoit plus la variable correspondante. Une solution pour (quand même) la recevoir peut être de "dédoubler" ce champ en générant (en plus) un champ de type hidden, c.a.d. un champ qu'on pré-remplit, qui fera partie du formulaire, mais sera invisible dans la page web.
    (affichez le code source en milieu de jeu pour mieux comprendre)

La correction se trouve ICI

Upload de Fichiers

Il arrive fréquemment que les applications Web permettent à leurs utilisateurs d'uploader des fichiers sur le serveur. C'est par exemple le cas d'images utilisées pour customiser son avatar sur un site communautaire, pour rendre des devoirs dans une application d'e-learning, etc.

Formulaire d'Uploading

La première étape de l'uploading consite à fournir à l'utilisateur le moyen de sélectionner le(s) fichier(s) qu'il souhaite uploader. La méthode la plus simple consiste à lui présenter un formulaire.

Le formulaire d'uploading utilise :
  • une method="POST"
  • un enctype="multipart/form-data"
  • un <input type="file" ... >
Un exemple :
upload_form.html
<!doctype html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Uploading</title>

    <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta2/dist/css/bootstrap.min.css"
          rel="stylesheet"
          integrity="sha384-BmbxuPwQa2lc/FVzBcNJ7UAyJxM6wuqIj61tLrc4wSX0szH/Ev+nYRRuWlolflfl"
          crossorigin="anonymous">
    <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta2/dist/js/bootstrap.bundle.min.js"
            integrity="sha384-b5kHyXgcpbZJO/tY9Ul7kGkf1S0CWuKcCD38l8YkeH8z8QjE0GmW1gYU5S9FOnJ0"
            crossorigin="anonymous"></script>

    <style>
        form{
            margin: 10px;
            width: 400px;
        }
        button{
            width: 100%;
        }
    </style>

</head>
<body>

    <!-- LE FORMULAIRE D'UPLOADING EST ICI : -->
    <form action="upload.php" method="POST" enctype="multipart/form-data">
        <div class="mb-3">
            <label for="le_fichier" class="form-label">Uploader un fichier :</label>
            <input type="file" class="form-control" id="le_fichier" name="le_fichier">
        </div>
        <div>
            <button type="submit" class="btn btn-primary">Submit</button>
        </div>
    </form>

</body>
</html>
Résultat

Données Reçues

Le formulaire que nous venons d'écrire indique (dans son attribut action) que, lorsqu'il est soumis, les données sont traitées par le script upload.php (à écrire).

Comme vous le savez déjà, les données envoyées par un formulaire utilisant la méthode POST sont reçues dans le script qui le traite au sein de la variable $_POST.

Si des données peuvent bien se trouver dans la variable $_POST, les données liée à l'input de type="file" se trouvent dans la variable $_FILE !

Pour le vérifier, nous pouvons écrire une première version de upload.php qui va simplement afficher le contenu de $_FILE

upload.php (v1)
<pre>
<?php

// Les informations liées au fichier uploadé se trouvent dans $_FILES

// Ici un simple dump des infos reçues :
print_r($_FILES);

// NB : on aurait aussi pu utiliser :
// var_dump($_FILES)

?>
</pre>
Testez avec le formulaire.
N'oubliez pas de sélectionner un fichier (pas trop gros).
NB : par défaut la taille du fichier pouvant être uploadé est limitée par PHP.

Vous devez voir s'afficher quelque chose du type :

Array
(
    [le_fichier] => Array
        (
            [name] => code_wall.jpg
            [type] => image/jpeg
            [tmp_name] => /Applications/MAMP/tmp/php/phph1LaNu
            [error] => 0
            [size] => 163169
        )

)

Détail des données reçues

En affichant les données reçues, vous aurez remarqué qu'on y trouve un tableau à l'index 'le_fichier'.

Le nom de cet index correspond à la valeur que vous avez donnée à l'attribut name de l'input de type type="file" du formulaire.

<input type="file" class="form-control" id="le_fichier" name="le_fichier">

Cet index (ici 'le_fichier') est utilisé pour accéder aux données liées au fichier qui a été uploadé. L'exemple de traitement ci-dessous (une 2ème version de upload.php) en détaille le contenu.

upload.php (v2)
<style>
    li{ margin-bottom: 10px; }
    code{ border-radius: 5px ; background: lightgrey ; padding: 5px }
</style>

<?php
// Les informations liées au fichier uploadé se trouvent dans $_FILES

$file = $_FILES['le_fichier']; // NB : 'le_fichier' est le name de votre input dans le formulaire
?>

Informations relatives au fichier reçu :
<ul>
    <li>
        Le nom original du fichier :
        <code> <?= $file['name'] ?> </code>
    </li>
    <li>
        Le type du fichier :
        <code> <?= $file['type'] ?> </code>
    </li>
    <li>
        Le fichier est enregistré dans le serveur sous un nom temporaire :
        <code> <?= $file['tmp_name'] ?> </code>
        <br>(il est automatiquement détruit à la fin de l'exécution du script)
    </li>
    <li>
        Un code d'erreur :
        <code> <?= $file['error'] ?> </code>
        <br>(si 0, tout va bien)
    </li>
    <li>
        La taille du fichier :
        <code> <?= $file['size'] ?></code> octets
    </li>
</ul>
Testez avec le formulaire.
N'oubliez pas de sélectionner un fichier (pas trop gros).
NB : par défaut la taille du fichier pouvant être uploadé est limitée par PHP.

Ces détails liés au fichier sont utiles pour effectuer des vérifications après lesquelles vous pouvez décider (ou non) d'enregistrer le fichier uploadé dans votre serveur pour qu'il en fasse partie...

Enregistrement dans le Serveur

Comme nous venons de le voir, un fichier uploadé est automatiquement et enregistré sous un nom temporaire dans le serveur. Le nom de ce fichier temporaire se trouve dans l'index 'tmp_name' des données relatives à ce fichier contenues dans $_FILES.

Dans notre exemple, on trouvera donc le nom de fichier temporaire dans :

$temp_file_name = $_FILES['le_fichier']['tmp_name'] // Rappel : 'le_fichier' est le name de l'input

Il est à noter que ce fichier temporaire sera automatiquement détruit par le serveur à la fin de l'exécution du script PHP qui traite le formulaire d'upload (donc ici le script upload.php).

Pour que le fichier temporaire devienne effectivement un fichier du serveur, il "suffit" d'en faire une copie.

La fonction PHP move_uploaded_file(string $from, string $to): bool vérifie que le fichier from est un fichier téléchargé par HTTP POST. Si le fichier est valide, la fonction renvoie True, et le fichier est copié dans to.

La copie peut alors être utilisée par vos (autres) scripts comme n'importe quel autre fichier du serveur.

Dans la 3ème version de notre script upload.php ci-dessous, le fichier temporaire uploadé est enregistré sous son nom original dans un sous-dossier que nous avons nommé uploads.

upload.php (v3)

<?php

// Les informations liées au fichier uploadé se trouvent dans $_FILES
// NB : 'le_fichier' est le nom du champ dans le formulaire

// On vérifie qu'un fichier a bien été mis dans le formulaire
if(empty($_FILES['le_fichier'])) die("<span style='color : red'>Il n'y a pas de fichier !</span>") ;

// On récupère les informations
$file = $_FILES['le_fichier'] ;

if($file['error'] == 0){ // S il n'y a pas d erreur

    // Pour sauvegarder le fichier uploadé, il faut faire une 'copie'
    // du fichier temporaire reçu dans un dossier local.

    // Nom du fichier temporaire reçu
    $temp_file_name = $file['tmp_name'] ;

    // Nom du fichier sauvegardé :
    // -> on peut utiliser le nom original du fichier
    $file_name = $file['name'] ;
    // -> mais on aurait pu ou choisir un autre nom :
    // $file_name = "truc_machin_chose.jpg" ;

    // Nom (arbitraire) du dossier où l'on enregistre la sauvegarde :
    $dir_name = "./uploads/" ;
    // Vérification d'existence (et éventuelle création) du dossier cible
    if(!is_dir($dir_name)) mkdir($dir_name) ;

    // Enregistrement du fichier dans le serveur :
    $full_name = $dir_name . $file_name ;
    move_uploaded_file($temp_file_name, $full_name) ; ?>

    <!-- Conclusion -->
    Le fichier a été uploadé dans : <code><?= $full_name ?></code>

    <!-- Bonus : si le fichier est une image, on l'affiche -->
    <?php
    $file_type = exif_imagetype($full_name) ;
    if ($file_type == IMAGETYPE_GIF || $file_type == IMAGETYPE_JPEG || $file_type == IMAGETYPE_PNG) :?>
        <img src="<?= $full_name ?>">
    <?php endif; ?>

<?php
} else { // Si une erreur s'est produite (ex. fichier trop gros, etc.) ?>

    <div style="color:red">
        Une erreur s'est produite (code : <?= $file['error'] ?>).<br>
        Le fichier n'a pas été correctement uploadé.
    </div>

<?php
}

Ce script :

  • Vérifie avec empty qu'un fichier a bien été envoyé avec le formulaire.
    (on arrête le script avec die si ce n'est pas le cas)
  • Met l'ensemble des informations dans $file.
  • S'il n'y a pas de code d'erreur dans $file:
    • Met le nom du fichier temporaire dans $temp_file_name.
    • Met le nom original du fichier dans $file_name : nous avons ici choisi d'enregistrer le fichier uploadé sous son nom original, mais nous aurions aussi tout à fait pu décider de lui donner un autre nom...
    • Vérifie avec is_dir si le dossier "./uploads/" (variable $dir_name) existe : sinon le dossier est créé avec mkdir.
    • Copie le fichier uploadé temporaire $temp_file_name sous le nom choisi $full_name (concaténation de $dir_name et $file_name) avec move_uploaded_file.
    • Affiche un message indiquant que le fichier a bien été uploadé sous le nom $full_name.
    • En bonus, vérifie avec exif_imagetype si le fichier uploadé est une image et, si tel est le cas, génère du code HTML utilisant ce "nouveau" fichier du serveur pour afficher cette image.
  • (S'il y avait un code d'erreur dans $file : on affiche que le fichier n'a pas été correctement uploadé).

NB1 : on aurait pu ajouter plus de tests et vérifications (je ne l'ai pas fait ici pour ne pas trop alourdir l'exemple...).

NB2: il est possible d'uploader plusieurs fichiers en même temps...

Variables Superglobales

Les superglobales sont des tableaux associatifs qui servent divers buts.

Elles sont automatiquement créées par PHP.

$GLOBALS

La variable $GLOBALS contient toutes les variables globales définies dans un script PHP.

Cette superglobale permet donc d'accéder aux variables globales dans des contextes plus restreints comme par exemple au sein d'une fonction.

Un exemple :

<?php
// définition d'une variable globale
$nickname = "Greg" ;

// NB: on aurait aussi pu faire :
// $GLOBALS['nickname'] = "Greg" ;

function disBonjour(): void{
//    echo "Bonjour " . $nickname ; // Génèrerait une ERREUR
    echo "Bonjour " . $GLOBALS['nickname'] ; // OK
}

// utilisation de la fonction
disBonjour() ;
?>
Résultat
Bonjour Greg

Le mot clé global

A l'intérieur d'une fonction, on peut aussi utiliser le mot clé global pour référencer une variable contenue dans $GLOBALS.

<?php
// définition d'une variable globale
$nickname = "Greg" ;

function disReBonjour(): void{
    global $nickname ; // indique que la variable a été définie en global
    echo "RE-Bonjour $nickname" ; // OK
}

// utilisation de la fonction
disReBonjour() ;
?>
Résultat
RE-Bonjour Greg

$_GET, $_POST, $_FILES, et $_REQUEST

Je les rappelle ici, mais nous avons déjà abordé ces 3 variables.

On ne s'attardera donc pas plus sur le sujet ;).

$_SERVER

La variable $_SERVER fournit diverses informations à propos du serveur ET du client (ex. adresse IP, port, type de navigateur, etc.).

Il serait fastidieux de toutes les énumérer, mais il est assez simple d'en avoir un aperçu : testez le code ci-dessous pour en voir le contenu.

<pre>
<?php
print_r($_SERVER) ;
?>
</pre>

Une démo affichant les informations sur VOTRE client pendant que vous lisez cette page:

Votre IP : <b><?php echo $_SERVER['REMOTE_ADDR']?></b><br>
Votre navigateur : <b><?php echo $_SERVER['HTTP_USER_AGENT'] ?></b>
?>
</pre>
Résultat
Votre IP : 52.14.126.74
Votre navigateur : Mozilla/5.0 AppleWebKit/537.36 (KHTML, like Gecko; compatible; ClaudeBot/1.0; +claudebot@anthropic.com) ?>

$_SESSION

La variable $_SESSION est très importante car c'est elle qui va vous permettre de conserver les données utilisateur d'un script à l'autre dans la même session.

Le problème

Comme vous avez du le remarquer, à chaque fois qu'un utilisateur "appelle" un script PHP, le contexte d'exécution (re)part de zéro: en d'autres termes, toutes les variables, fonctions, etc. n'existent plus et il faut tout recharger/recréer.

On a aussi vu qu'avec une requête GET ou POST, on arrive à faire passer des données d'une page (contenant par ex. un formulaire) à une autre (traitant les données reçues par GET ou POST).

Imaginez une page HTML login_form.html contenant un formulaire qui récupère le (login) d'un utilisateur.

login_form.html
<form id="user-infos" class="card" action="login.php">
    <input type="text" class="form-control" placeholder="login" name="login">
    <button type="submit" class="btn btn-primary">LOGIN</button>
</form>      

Ce formulaire a pour action le script login.php qui traite les données reçues, c.a.d. la variable $_GET['login'] : il affiche un message de confirmation reprenant le login (qu'on vient d'avoir dans $_GET) + un lien vers l'espace client de l'utilisateur.

login.php
<h1>Bonjour <b><?php echo htmlspecialchars($_GET['login']) ?></b> !</h1>
<p>
    Suivez ce lien pour accéder à votre <a href="espace_client.php">espace client</a>.
</p>

Jusque là, tout fonctionne...


On voit que l'espace client est géré par le script espace_client.php : on voudrait alors y afficher un message du type "Espace Client de prénom nom".

Cependant, si vous écrivez naïvement le code :
espace_client.php
<h1>
    Espace client de <?php echo htmlspecialchars($_GET['login']) ?>
</h1>

... et bien cela ne fonctionnera pas car $_GET est vide !

En effet, espace_client.php n'a pas été appelé par un formulaire et de ce fait, il ne reçoit pas de données dans $_GET, $_POST ou encore $_REQUEST.

La solution : $_SESSION

Pour résoudre ce type de problème, PHP met à disposition le mécanisme de session qui permet d'enregistrer et récupérer des données liées à l'utilisateur qui visite le site.

Ces données sont stockées dans la superglobale $_SESSION.

Il y aura 1 $_SESSION par utilisateur.
PHP qui gère automatiquement le lien entre un utilisateur et sa session.

Quand un utilisateur "navigue" sur un de vos scripts, PHP lui associe un objet session, et quand vous faites appel à $_SESSION dans votre code, PHP fournit automatiquement le $_SESSION de l'utilisateur qui a "demandé" ce script.

En d'autres termes, des utilisateurs différents qui naviguent sur le même script n'obtiendront pas le même résultat puisque le contenu de leur $_SESSION sera différent.

Démarre ou récupérer une session : session_start()

Pour avoir accès à $_SESSION au sein d'un script, il faut tout d'abord à chaque fois faire appel à la fonction session_start() : cela démarre ou récupère la session de l'utilisateur connecté.

Pour que notre exemple précédent fonctionne, il aurait fallu enregistrer dans la $_SESSION de l'utilisateur les données (qu'il a) fournies dans $_GET au moment où elles étaient disponibles, c.a.d. ici dans login.php.

NB: les scripts ci-dessous sont simplifiés pour l'exemple: ils manquent de vérifications!

Le nouveau code est alors :
login.php
<?php
session_start() ; // démarrage/récupération de la sesssion

if (isset($_GET['login'])) {
    $login = $_GET['login'];

    // on enregistre ce qui nous intéresse dans la session de l'utilisateur
    $_SESSION['login'] = $login ;
}
?>

<h1>Bonjour <b><?php echo htmlspecialchars($login) ?></b> !</h1>
<p>
    Suivez ce lien pour accéder à votre <a href="espace_client.php">espace client</a>.
</p>

Et on peut maintenant écrire :
espace_client.php
<?php
session_start(); // récupération de la session de l'utilisateur

// récupération des données de cette session :
$login = $_SESSION['login'] ;
?>

<h1>
    Espace client de <?php echo htmlspecialchars($login) ?>
</h1>
<hr>
<p>
    <a href="logout.php">LOUGOUT</a>
</p>

NB: pour détruire une variable de session, on utilisera le classique unset sur la clé de $_SESSION visée (ex. unset($_SESSION['login']).


Détruire une session : session_destroy()

Les données de l'utilisateur stockées dans $_SESSION sont disponibles dans tous les scripts du site utilisant session_start(), ceci jusqu'à ce que sa session soit détruite (par exemple suite à la fermeture du navigateur).

La fonction PHP session_destroy() permet explicitement de détruire une session.
(elle doit être appelée après avoir récupéré la session avec session_start())

Vous avez certainement noté que dans le code de espace_client.php, il y a un lien vers le script logout.php : ce script a pour but de détruire la session de l'utilisateur.

logout.php
<?php
// récupération de la session de l'utilisateur
session_start() ;

// destruction de la session (et donc du contenu de $_SESSION)
session_destroy() ;
?>

<h1>Vous êtes déconnecté.</h1>

<a href="login_form.html">Retour au début.</a>

NB: il est possible de définir un session timeout (une durée maximale de validité des sessions) en utilisant php.ini, ou en l'implémentant vous même en PHP par exemple en utilisant la fonction time + vérification adhoc à chaque démarrage de script.

Redirections

Dans la plupart de nos pages et scripts, nous avons pour l'instant utilisé des ancres et des actions de formulaire pour diriger l'utilisateur d'un script ou d'une page à l'autre.

Il peut cependant souvent être pratique de créer des scripts qui effectuent un traitement spécifique sans sortie (HTML) et qui, une fois le traitement terminé, redirigent automatiquement l'utilisateur vers une autre page (.php ou .html).

TP 04