Formulaire PHP

Scénario : Lorsque le client aura complété le formulaire, il va cliquer sur un bouton « Envoyer » (les réponses sont alors envoyées au serveur) puis  :

  • soit le client reste sur la même page PHP : on recharge la même page PHP. Généralement, en haut on analyse des réponses du formulaire précédemment envoyées, puis en bas de page on propose à nouveau le formulaire.
  • soit on dirige le client sur une nouvelle page PHP, et c’est cette nouvelle page qui analyse des réponses du formulaire précédemment envoyées.

Base de données MySQL

Quote inversée `

Un peu plus loin, dans les requêtes SQL, vous verrez l’utilisation de quotes inversées (backtick ou backquote) autour du nom de la table et de ses colonnes.
Pour taper une quote inversée : [Alt Gr]+7  puis un espace (pour Apple : sur la touche £ suivi d’un espace).

Un identicateur est limité à 30 caractères, et commence par une lettre. Les lettres accentuées ne sont pas acceptées. En revanche, les 3 symboles # , $, _ sont acceptés.

MySQL ne tient pas compte de la casse (donc ne différencie pas les lettres minuscules et majuscules).

Ouverture de la BDD

style Mysqli procédural

<?php
// OUVERTURE DE LA BASE DE DONNEES
$db = mysqli_connect('localhost', 'my_user', 'my_password', 'my_db');
 
// Facultatif : En cas d'échec : message d'erreur puis arrêt du script
if (!$db) die('ERREUR n°'.mysqli_connect_errno().' Info = '. mysqli_connect_error());
echo 'Connexion réussie. Info = '.mysqli_get_host_info($db)."<br>";
 
// EXPLOITATION DES TABLES DE LA BASE DE DONNEES 'my_db'
//  ... futur code à insérer ici ...
 
// FERMETURE DE LA BASE LA BASE DE DONNEES
mysqli_close($db);
?>

die() : écrit le message et arrête le script.

style Mysqli orienté objet

Ou bien, la même chose en un style Orienté Objet :

<?php
// OUVERTURE DE LA BASE DE DONNEES
$db = new mysqli('localhost', 'my_user', 'my_password', 'my_db');
if ($db->connect_error) die('ERREUR n°'.$db->connect_errno.' * Info = '.$db->connect_error);
echo 'Connexion réussie. Info = '. $db->host_info . "<br>";
 
// EXPLOITATION DES TABLES DE LA BASE DE DONNEES 'my_db'
//  ... futur code à insérer ici ...
 
// FERMETURE DE LA BASE LA BASE DE DONNEES
$db->close();
?>

style PDO

<?php
try {
  // OUVERTURE DE LA BASE DE DONNEES
  $db = new PDO('mysql:host=localhost; dbname=my_db', 'my_user', 'my_password');

  // EXPLOITATION DE LA BASE DE DONNEES 'my_db'
  // ... futur code à insérer ici ...

  // FERMETURE DE LA BASE LA BASE DE DONNEES
  $db->close();
}
catch (Exception $e) {
  die('Erreur : ' . $e->getMessage());
}
?>

Créer et compléter une TABLE avec Adminer

Dans la BDD, on crée un ou plusieurs tableaux (appelés TABLE). Le plus simple est d’utiliser l’interface Adminer ou PhpMyAdmin.

cocher NULL  : si cette colonne peut rester vide (si elle concerne une information facultative)
cocher AI : si les valeurs de cette colonne doit être incrémentée automatiquement.

Exemple : 2 TABLES : moteur = InnoDB  et  interclassement = utf8_general_ci

  • table ‘user’ avec 3 colonnes : ID ; mail ; pass
  • table ‘data’ avec 4 colonnes : ID ; content ; x ; y

Généralement, le nom d’une table est au singulier.
SMALL INT : entier sur 2 octets, pourvant aller de -32 768 à 32 767.
INTEGER : entier sur 4 octets, pouvant aller de -2 147 483.648 à 2 147 483 647.
BIT : pour les booléens (car 1 bit).
DECIMAL(8,2) : nombre décimal avec au maximum 2 décimales et 8 chiffres au total (donc 6 chiffres avant la virgule). Exemple : -10, 2.5, 1.2E-8 (c’est-à-dire 1.2 x 10−8).

Ce qui donne en requête SQL :

CREATE DATABASE `OIB` ;
CREATE TABLE `user` (
    `ID` INT( 11 ) NOT NULL ,
    `mail` VARCHAR( 255 ) NOT NULL ,
    `pass` VARCHAR( 255 ) NOT NULL ,
    PRIMARY KEY ( `ID` ));
INSERT INTO `user` ( `id` , `mail` , `pass`) VALUES 
    ('1', 'test@test.fr', 'mdp'), 
    ('2', 'test2@test.fr', 'mdp2');

Récupérer un tableau entier SELECT * FROM

Pas de point-virgule à la fin d’une requête SQL.

Le but de cette partie est de récupérer toute la table 'data' en un tableau 2D  $data (php).
Plusieurs méthodes sont possibles …

style Mysqli procédural

Nécessite le module Mysqlnd (Mysql native driver) car on utilise mysqli_fetch_all() avec Mysqli.

<?php
// OUVERTURE DE LA BASE DE DONNEES
$db = mysqli_connect('localhost', 'my_user', 'my_password', 'my_db');

// LA REQUETE : RECUPERER TOUS LES ELEMENTS DE LA TABLE 'data'
//la requête MySQL
$sql = 'SELECT `name`,`age`,`gender` FROM `data`';

//on envoie la requête et récupère les résultats
$resultat = mysqli_query($db,$sql); 

//Facultatif : le nb de lignes du tableau
$nbArticles = mysqli_num_rows($resultat);

//va chercher TOUS les $resultat sous forme de tableau associatif (en tableau 2D) $data
$data = mysqli_fetch_all($resultat,MYSQLI_ASSOC);

// FERMETURE DE LA BDD et LIBERE LA MEMOIRE
mysqli_free_result($resultat);
mysqli_close($db);

//Facultatif : AFFICHAGE DU TABLEAU 2D : $data
echo "<p>Il y a $nbArticles lignes.</p>";
foreach($data as $ligne){
  foreach($ligne as $cle=>$valeur){
    echo $cle.': '.$valeur.'   |   ';
  }
  echo "<br>";
}
?>

Variante sans mysqli_fetch_all()  (cette variante ne nécessite donc pas le module Mysqlnd) ; on traite alors ligne par ligne par une boucle while et l’une de ces 3 fonctions :

  • mysqli_fetch_assoc()   Récupère une ligne de résultats sous forme de tableau associatif (la colonne du tableau porte le nom de la colonne de la table).
  • mysqli_fetch_row()   Récupère une ligne de résultats sous forme de tableau indexé (à indice numérique : 0, 1, …).
  • mysqli_fetch_array()   Récupère une ligne de résultats sous forme d’un tableau à indices numériques ou associatif (avec l’argument MYSQLI_NUM ou MYSQLI_ASSOC ou MYSQLI_BOTH).
<?php
// OUVERTURE DE LA BASE DE DONNEES
$db = mysqli_connect('localhost', 'my_user', 'my_password', 'my_db');
 
// LA REQUETE : RECUPERER TOUS LES ELEMENTS DE LA TABLE 'data'
//la requête MySQL
$sql = 'SELECT `name`,`age`,`gender` FROM `data`';
 
//on envoie la requête et récupère les résultats
$resultat = mysqli_query($db,$sql);
 
//Facultatif : le nb de lignes du tableau
$nbArticles = mysqli_num_rows($resultat);
 
//va chercher chaque ligne de $resultat en un tableau 1D associatif $row
//et on insère chaque ligne $row dans un tableau 2D $data
while( $row = mysqli_fetch_assoc($resultat) )
   $data[] = $row;

// FERMETURE DE LA BDD et LIBERE LA MEMOIRE
mysqli_free_result($resultat);
mysqli_close($db);
?>

style Mysqli orienté objet

<?php
// OUVERTURE DE LA BASE DE DONNEES
$db = mysqli_connect('localhost', 'my_user', 'my_password', 'my_db');

// LA REQUETE : RECUPERER TOUS LES ELEMENTS DE LA TABLE 'data'
//la requête MySQL
$sql = 'SELECT `name`,`age`,`gender` FROM `data`';

//on envoie la requête et récupère les résultats
$resultat = $db->query($sql);

//Facultatif : le nb de lignes du tableau
$nbArticles = $resultat->num_rows;

//va chercher TOUS les $resultat sous forme de tableau associatif (en tableau 2D) $data
$data = $resultat->fetch_all(MYSQLI_ASSOC);

// FERMETURE DE LA BDD et LIBERE LA MEMOIRE
$resultat->close();
$db->close;

//Facultatif : AFFICHAGE DU TABLEAU 2D : $data
echo "<p>Il y a $nbArticles lignes.</p>";
foreach($data as $ligne){
  foreach($ligne as $cle=>$valeur){
    echo $cle.': '.$valeur.'   |   ';
  }
  echo "<br>";
}
?>

Variante sans fetch_all()  (cette variante ne nécessite donc pas le module Mysqlnd) ; on traite alors ligne par ligne par une boucle while et l’une de ces 3 fonctions :

  • fetch_assoc()   Récupère une ligne de résultats sous forme de tableau associatif (la colonne du tableau porte le nom de la colonne de la table).
  • fetch_row()   Récupère une ligne de résultats sous forme de tableau indexé (à indice numérique : 0, 1, …).
  • fetch_array()   Récupère une ligne de résultats sous forme d’un tableau à indices numériques ou associatif
<?php
// OUVERTURE DE LA BASE DE DONNEES
$db = mysqli_connect('localhost', 'my_user', 'my_password', 'my_db');

// LA REQUETE : RECUPERER TOUS LES ELEMENTS DE LA TABLE 'data'
//la requête MySQL
$sql = 'SELECT `name`,`age`,`gender` FROM `data`';

//on envoie la requête et récupère les résultats
$resultat = $db->query($sql);

//Facultatif : le nb de lignes du tableau
$nbArticles = $resultat->num_rows;

//va chercher chaque ligne de $resultat en un tableau 1D associatif $row
//et on insère chaque ligne $row dans un tableau 2D $data
while( $row = $resultat->fetch_assoc() )
   $data[] = $row;

// FERMETURE DE LA BDD et LIBERE LA MEMOIRE
$resultat->close();
$db->close;
?>

style Mysqli orienté Objet et préparé

Nécessite le module Mysqlnd (Mysql native driver) car on utilise get_result() et aussi fetch_all() avec Mysqli.

<?php
// OUVERTURE DE LA BASE DE DONNEES
$db = new mysqli('localhost', 'my_user', 'my_password', 'my_db');

// REQUETE PREPAREE : RECUPERER TOUS LES ELEMENTS DE LA TABLE 'data'
//la requête MySQL
$sql = 'SELECT `name`,`age`,`gender` FROM `data`';

//préparation, exécution et résultat
$stmt = $db->prepare($sql);
$stmt->execute();
$resultat = $stmt->get_result();

//Facultatif : le nb de lignes :
$nbArticles = $resultat->num_rows;

//va chercher TOUS les $resultat sous forme de tableau 2D associatif $data
$data = $resultat->fetch_all(MYSQLI_ASSOC);

// FERMETURE DE LA BDD et LIBERE LA MEMOIRE
$stmt->free_result();
$stmt->close();
$db->close;

//Facultatif : AFFICHAGE DU TABLEAU 2D : $data
echo "<p>Il y a $nbArticles lignes.</p>";
foreach($data as $ligne){
  foreach($ligne as $cle=>$valeur){
    echo $cle.': '.$valeur.'   |   ';
  }
  echo "<br>";
}
?>

Variante sans fetch_all() et get_result() (cette variante ne nécessite donc pas le module Mysqlnd) :

<?php
// OUVERTURE DE LA BASE DE DONNEES
$db = new mysqli('localhost', 'my_user', 'my_password', 'my_db');

// REQUETE PREPAREE : RECUPERER TOUS LES ELEMENTS DE LA TABLE 'data'
//la requête MySQL
$sql = 'SELECT `name`,`age`,`gender` FROM `data`';

//préparation, exécution, liaison avec des variables php
$stmt = $db->prepare($sql);
$stmt->execute();
$stmt->bind_result($name, $age, $gender);

//Facultatif : le nb de lignes (grâce à l'enregistmt des propriétés store_result)
$stmt->store_result();
$nbArticles = $stmt->num_rows;

//va chercher chaque ligne et les insère dans les variables php $name, $age, $gender
//et on insère ces variables php dans un tableau associatif 2D $data
while ($stmt->fetch())
  $data[]=array('name'=>$name, 'age'=>$age, 'gender'=>$gender);

// FERMETURE DE LA BDD et LIBERE LA MEMOIRE
$stmt->free_result();
$stmt->close();
$db->close;
?>

style PDO et préparé

<?php
// OUVERTURE DE LA BASE DE DONNEES
$db = new PDO('mysql:host=localhost; dbname=my_db', 'my_user', 'my_password');

// REQUETE PREPAREE : RECUPERER TOUS LES ELEMENTS DE LA TABLE 'data'
//la requête MySQL
$sql = 'SELECT `name`, `age`, `gender` FROM `data`';

//préparation et exécution
$stmt = $db->prepare($sql);
$stmt->execute();

//Facultatif : nb de lignes (pour MySQL : rowCount() peut remplacer le numRow() de Mysqli)
$nbArticles = $stmt->rowCount();

//va chercher TOUS les resultats sous forme de tableau 2D associatif $data
$data = $stmt->fetchAll(PDO::FETCH_ASSOC);

// FERMETURE DE LA BDD et LIBERE MEMOIRE
$stmt->closeCursor();
$stmt = null;
$db = null;

//Facultatif : AFFICHAGE DU TABLEAU 2D : $data
echo "<p>Il y a $nbArticles lignes.</p>";
foreach($data as $ligne){
  foreach($ligne as $cle=>$valeur){
    echo $cle.': '.$valeur.'   |   ';
  }
  echo "<br>";
}
?>

Pourquoi préparer une requête

Plus professionnellement, on privilégie la préparation / exécution de requête, car cette méthode permet :

  • d’envoyer séparément les variables et la requête au serveur : davantage de sécurité.
  • de répéter les éxécutions de requête pour plusieurs séries de variables (avec une seule préparation) : grande efficacité.
<?php
// OUVERTURE DE LA BASE DE DONNEES
$db = new mysqli('localhost', 'my_user', 'my_password', 'my_db');

// 1 - PREPARATION D'UNE REQUETE ANONYME
//une requête avec 2 paramètres (les ?) : le début du 'name' et l'âge maximum
$sql = 'SELECT `name`,`gender` FROM `data` WHERE `name` LIKE CONCAT(?,"%") AND `age` <= ?';
// 

//prépare cette requête (avec les 2 paramètres inconnus) et crée un template $stmt
$stmt = $db->prepare($sql);

// 2 - LIAGE : donner les VALEURS DES PARAMETRES (bind_param)
//D'abord on relie les paramètres inconnus (les ?) et leurs valeurs "T" et 18
//"si" représente leur type dans l'ordre :
// s = string  : pour le 1er paramètre
// i = integer : pour le 2e  paramètre (il existe aussi : nombre décimal : d = double)
$debut_du_nom = "T";
$limite_age = 18;
$stmt->bind_param('si', $debut_du_nom , $limite_age);

// 3 - EXECUTION DE REQUETE
//SELECT `name`, `age`, `gender` FROM `data2` WHERE `name` LIKE "T%" AND `age` <= 18
//recherche de nom et genre des clients avec un nom commencant par T et inférieur à 18 ans
$stmt->execute();

// 4 - LIAGE des VALEURS DES RESULTATS à des VARIABLES PHP (bind_result)
$name   = NULL;
$gender = NULL;
$stmt->bind_result($name, $gender);

// 5 - "VA CHERCHER"(fetch) CHAQUE LIGNE DE RESULTAT [mise dans les variables php]
while ( $stmt->fetch() ) {
  echo "<br> * nom : $name de genre : $gender";
}

// FERMETURE DE LA BDD et LIBERE LA MEMOIRE
$stmt->free_result();
$stmt->close();
$db->close;
?>

Requête précise SELECT .. FROM

toutes les colonnes *

$sql = 'SELECT * FROM `data`';    récupère le tableau entier (* signifie « toutes les colonnes »).

filtre WHERE

Mettre des quotes (' ou ") autour des noms (texte), sinon ils sont considérés comme des colonnes.
On utilise les opérandes :  =, != (différent), >, >=, <, <= , BETWEEN et aussi AND, OR, NOT.

Si plusieurs tables ont des colonnes avec des noms identiques alors une colonne `name` doit être précédée du nom de sa table, donc `data.name` pour lever toute ambiguité.

  • $sql = 'SELECT `name`, `age`, `gender` FROM `data` WHERE `age`<=20';    récupère un tableau 3 colonnes (name, age, gender) dont la colonne ‘age’ est inférieure à 20 ans.
  • $sql = 'SELECT `name` FROM `data` WHERE `age` BETWEEN 18 AND 20';    pour récupérer un tableau des noms (1 colonne) dont la colonne ‘age’ est compris entre 18 (inclus) et 20 ans (inclus). Le contraire existe aussi : WHERE `age` NOT BETWEEN 18 AND 20
  • $sql = 'SELECT * FROM `data` WHERE `age` IN (18,20,21)';    pour récupérer un tableau dont la colonne ‘age’ est 18 ou 20 ou 21 ans. Le contraire existe aussi : WHERE NOT `age` IN (18,20,21)
  • $sql = 'SELECT * FROM `data` WHERE (`age` BETWEEN 18 AND 20) AND NOT `gender`="female"';    pour récupérer un tableau des hommes agés entre 18 et 20 ans.

ordre d’affichage ORDER BY

  • ordre croissant : $sql = 'SELECT * FROM `data` WHERE `gender`="female" ORDER BY `name`';    pour récupérer un tableau ordonné par ordre alphabétique / numérique croissant du nom.
  • ordre décroissant (de Z à A) en ajoutant DESC : $sql = 'SELECT * FROM `data` ORDER BY `name` DESC';

quota / paginer LIMIT

  • LIMIT 0, 20 : affiche les 20 premiers résultats
  • LIMIT 5, 10 : affiche de la sixième à la quinzième entrée : 10 résultats
  • LIMIT 10, 2 : affiche la onzième et la douzième entrée : 2 résultats

$sql = 'SELECT * FROM `data` LIMIT 0, 20';

selon un modèle LIKE

$sql = 'SELECT `name`, `age`, `gender` FROM `data` WHERE `name` LIKE "G%" ';  pour rechercher les enregistrements dont le nom commence par un G (majuscule ou minuscule).

  • LIKE 'G%'  signifie que le caractère G peut être suivi de caractères (Gaston, Girafe, Gem …)
  • LIKE '%RO%' signifie la syllabe RO peut être précédée ou suivie de caractères (harold, rose …).

formatter les dates DATE_FORMAT()

$sql = 'SELECT *,DATE_FORMAT(`date_debut`, '%d-%m-%Y à %Hh%imin') as date_debut_fr,
         DATE_FORMAT(`date_fin`, '%d-%m-%Y à %Hh%imin') as date_fin_fr FROM `data`';

MySQL met en forme les dates demandées par la requête (date de début et fin) dans un style « 15-02-2000 à 18h04 ».

Autre solution : le timestamp : un horodatage qui associe une date à un nombre entier, il est facile à manipuler dans les calculs (écart de dates, moyenne …) car il évite de prendre en compte les années bissextiles …

Le timestamp d’une date est le nombre de secondes écoulées entre cette date et le 1er janvier 1970 à 0h00:00 (début de l’heure UNIX).

Ajouter un nouvel enregistrement

Simple

<?php
// OUVERTURE DE LA BASE DE DONNEES
$db = mysqli_connect('localhost', 'my_user', 'my_password', 'my_db');

// LA REQUETE : AJOUTER UN NOUVEL ENREGISTREMENT
$sql = INSERT INTO `data` (`name`, `age`, `comment`) VALUES ('test', '18', '')";
if (mysqli_query($db, $sql)) {
    echo "New record created successfully.";
} else {
    echo "Error: " . $sql . "<br>" . mysqli_error($db);
}

// FERMETURE DE LA BASE LA BASE DE DONNEES
mysqli_close($db);
?>

Par formulaire

Exemple de formulaire permettant d’ajouter un nouveau membre à la base de données.

CREATE TABLE `membres` (
    `id` INT NOT NULL PRIMARY KEY AUTO_INCREMENT,
    `identifiant` VARCHAR(50) NOT NULL UNIQUE,
    `mdp` VARCHAR(255) NOT NULL,
    `date` DATETIME DEFAULT CURRENT_TIMESTAMP
);


trim() : Supprime les espaces et caractère invisible en début et fin de chaîne : espace, tabulation (\t), nouvelle ligne (\n) , retour chariot (\r), caractère nul (\0), tabulation verticale (\x0B).

<?php
function test_input($data) {
  $data = trim($data);
  $data = stripslashes($data);
  $data = htmlspecialchars($data);
  return $data;
}

$message      = "";
$identifiant  = test_input($_POST["identifiant"]);
$mdp          = test_input($_POST["mdp"]);
$confirme_mdp = test_input($_POST["confirme_mdp"]);

if ($_SERVER["REQUEST_METHOD"] == "POST" ) {   // VERIFICATIONS EN CASCADE
  //-- 1/ Vérification de l'identifiant
  if (empty($identifiant)) { // si l'identifiant est vide
    $message = "Entrer un identifiant.";
  } else {
    $db = new mysqli('localhost', 'my_user', 'my_password', 'my_db');
    $sql = 'SELECT `id` FROM `membres` WHERE `identifiant` = ?';
    $stmt = $db->prepare($sql);
    $stmt->bind_param('s', $identifiant);
    $stmt->execute();
    $stmt->store_result(); //enregistre les propriétés des résultats
    $nbMembres = $stmt->num_rows;
    $stmt->free_result();
    $stmt->close();
    $db->close;
    if ($nbMembres == 1) {
      $message = "Cet identifiant <i>$identifiant</i> existe déjà.";
    } else { // bien ... l'identifiant est OK
      //-- 2/ Vérification du Mot De Passe
      if (empty($mdp)) { // si le mdp est vide
        $message = "Entrer un mot de passe.";
      } elseif (strlen($mdp) < 6) {
        $message = "Le mot de passe doit comporter au moins <i>6 caractères</i>.";
      } else { // bien ... le mdp est OK
        //-- 3/ Vérification de la confirmation du MDP
        if (empty($confirme_mdp)) {
          $message = "Il faut retaper le mot de passe.";
        } elseif ($mdp != $confirme_mdp) {
            $message = "Les 2 saisies du mot de passe ne correspondent pas.";
        } else { // identifiant + mdp + confirm_mdp sont OK ... on enregistre le membre
          $db = new mysqli('localhost', 'my_user', 'my_password', 'my_db');
          $sql = "INSERT INTO `membres` (`identifiant`, `mdp`) VALUES (?, ?)";
          $stmt = $db->prepare($sql);
          $stmt->bind_param('ss', $param_identifiant, $param_mdp);
          $param_identifiant = $identifiant;
          $param_mdp = password_hash($mdp, PASSWORD_DEFAULT);// hache le mdp (cryptage)
          if ( $stmt->execute() ) $message = "L'enregistrement s'est bien passé.";
          $stmt->close();
          $db->close;
        }
      }
    }
  }
}
?>
<!DOCTYPE html>
<html lang="fr">
  <head>
    <meta charset="utf-8" />
    <title>Sign Up</title>
  </head>
  <body>
<?php if (isset($message) && !empty($message)) echo "<hr><p><b>$message</b></p><hr>"; ?>
    <h1>Formulaire d'enregistrement 'Sign Up'</h1>
    <form method="post" action="<?php echo htmlspecialchars($_SERVER["PHP_SELF"]);?>">
      Identifiant : <input type="text" name="identifiant"><br />
      Mot de passe : <input type="text" name="mdp"><br />
      Retaper le mot de passe : <input type="text" name="confirme_mdp"><br />
      <input type="submit" name="submit" value="Envoyer">
    </form>
  </body>
</html>

Sécuriser MySQL

des injections SQL

Pour hacker la page d’identification (Log in) il suffit de mettre dans le champs ‘identifiant’ une condition SQL qui est toujours vraie (injection SQL) comme ' OR 1 = 1 --  car avec :

  • l’identifiant : $id = ' OR 1 = 1 --';
  • le mot de passe : $mdp = '1234';
  • la requête $sql = "SELECT `id`,`mdp` FROM `membres` WHERE `id` = '$id' AND `mdp` = '$mdp'";

On obtient SELECT `id`,`mdp` FROM `membres` WHERE `id` = '' OR 1 = 1 -- ' AND `mdp` = '1234'
et sachant que -- est le symbole de commentaire SQL alors la condition du mot de passe est ignorée !

Donc il ne reste que :SELECT `id`,`mdp` FROM `membres` WHERE `id` = '' OR 1 = 1 et cette requête renvoie tous les membres car la condition OR 1 = 1 est toujours vraie.

Bingo ! Le client est perçu comme un membre.

Pour éviter les injection de SQL, on utilise mysql_real_escape_string () (la fonction échappe les caractères malicieux) :

  • l’identifiant : $id = mysql_real_escape_string ( $_POST["id"] );
  • le mot de passe : $mdp = mysql_real_escape_string ( $_POST["mdp"] );

Ainsi on obtient : SELECT `id`,`mdp` FROM `membres` WHERE `id` = '\' OR 1 = 1 -- ' AND `mdp` = '1234' sachant que \' est une quote échappée (elle est traitée comme une partie du nom de l’identifiant = « ' OR 1 = 1 --« ) donc le mot de passe n’est pas commenté cette fois !

XSS (Cross-site Scripting)

Un visiteur malintentionné peut aussi injecter un script dans le champs identifiant, dans le but de voler un cookie, un token pour usurper une identité.

  • l’identifiant : $id = '<script>alert('BOUM !');</script>';

Ainsi echo $id; devient echo "<script>alert('BOUM!');</script>"; et donc le script s’exécute (ici, c’est juste une pop-up).

Pour éviter les injections de scripts, on utilise au choix :

  •  strip_tags() : la fonction supprime toutes les balises HTML.
    $id = strip_tags($_POST["id"]); donne $id = 'alert('BOUM !');'; : les balises <script> ont été supprimées.
  • htmlspecialchars() : la fonction convertit les caractères spéciaux “,&,<,> en entités HTML. Pour encoder la quote ‘ on ajoute le paramètre ENT_QUOTES.
    $id = htmlspecialchars($_POST["id"],ENT_QUOTES,'UTF-8'); donne $id = '&lt;script&gt;alert('BOUM !');&lt;/script&gt;'; : les balises <script> ont été supprimées.
    La fonction inverse htmlspecialchars_decode() convertit les entités HTML en caractères spéciaux.
  • htmlentities() : similaire à htmlspecialchars() sauf qu’elle convertit tous les caractères qui possèdent des entités HTML équivalentes.
    $id = htmlentities($_POST["id"],ENT_QUOTES,'UTF-8');
    La fonction inverse est html_entity_decode().
  • filter_var() : idem
    $id = filter_var($_POST["id"],FILTER_SANITIZE_FULL_SPECIAL_CHARS);

Ainsi le Javascript n’est pas exécuté !!!

autre fonction : urlencode() : pour encoder une URL : urlencode("https://isn.ovh") donne https%3A%2F%2Fisn.ovh. Et sa fonction inverse : urldecode().

basename() et realname() : pour récupérer un fichier en évitant la faille ‘directory transversal’.

hacher les mots de passe

On évite d’enregistrer les mots de passe en clair dans la BDD, mais on les crypte (hash). Puis lorsqu’on demande le mot de passe du client, on le crypte aussi, et si les 2 cryptages sont identiques alors c’est OK.

L’idéal est trouver une méthode de cryptage inviolable, c’est-à-dire sans décryptage possible (puisque ce sont les versions cryptées qui sont comparées). Si le hacker ne parvient pas à décrypter le mot de passe, celui-ci est inutilisable : effectivement, il faut absolument rentrer le mot de passe en clair pour que la comparaison soit validée.

Le MD5 et SHA1 sont très répandus mais faibles (crackables).
ex : md5('j0hnny140') donne 3451f3e47aaa22beee5af1ebf5ae6828.

Un bon cryptage utilise 2 choses : un  Salt (sel) combiné à un hash (algorithme de cryptage). Le Salt est une chaîne de caractère ajoutée AVANT le hashage.

password_hash() donne un cryptage fort Blowfish de 60 caractères avec un Salt auto-généré.
password_hash('j0hNny$140!', PASSWORD_DEFAULT); donne $2y$10$VNA4syJiHTI6XXQVQCpZX.amjcHOjOZxIUImLafsSxedGZkumFVqC

password_verify() permet de comparer le hash.

 

$password = 'j0hNny$140!';
$hash = '$2y$10$VNA4syJiHTI6XXQVQCpZX.amjcHOjOZxIUImLafsSxedGZkumFVqC';
if(password_verify($password, $hash)) echo "c'est le bon mot de passe";

 

PHP : session

Pour envoyer des informations au serveur, on utilise, au choix :

  • la méthode GET en modifiant l’url  page.php?age=18
  • la méthode POST avec un formulaire <form>

Mais pour passer des infos d’une page à une autre, le plus simple est l’utilisation de variables superglobales (les variables de session) … en créant une session pour chaque nouveau visiteur.

ouvrir une session

En fait, PHP envoie un identifiant de session sur l’ordinateur du client, puis chaque session_start() scanne l’ordi à la recherche d’un identifiant. S’il n’en trouve pas alors il en crée un nouveau.

  • Il faut appeler session_start()  sur chaque page AVANT le moindre code HTML (avant même le  <!doctype>).
  • Idem .. Interdit d’utiliser echo pendant les manipulations de session_start() … donc stocker les informations dans une variable $message et faire echo $message; plus tard.
  • Si on oublie session_start() sur une page, on ne peut pas accéder aux variables superglobales  $_SESSION .
<?php
session_start(); // continue ou démarre une session (à mettre AVANT le doctype)
?>

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Titre : page.php</title>
  </head>
  <body>
<?php
$_SESSION['prenom'] = 'Jean'; // création de variables de session
$_SESSION['age'] = 24;
?>
    <p>
      Bonjour <?php echo $_SESSION['prenom']; ?> !<br />
      <a href="page2.php">Lien vers page2.php</a>
    </p>
  </body>
</html>

Puis sur une autre page on récupère les variables de session :

<?php
session_start(); // permet de continuer la session (à mettre AVANT le doctype)
?>
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Titre : page2.php</title>
  </head>
  <body>
    <p>
      Sur cette autre page, je récupère les variables de session : <br />
      prénom :  <?php echo {$_SESSION['prenom']}; ?> !<br />
      age : <?php echo $_SESSION['age']; ?>
    </p>
  </body>
</html>

fermer la session

Lien ‘déconnexion’ (log out)

Ajouter un lien <a href="deconnexion.php">Déconnexion</a> sur toutes les pages du site. Et créer la page deconnexion.php .

<?php
session_start(); //continue ou démarre une session
$_SESSION = array(); //détruit les variables de session
if (ini_get('session.use_cookies')) { //et le cookie de session (expiration)
  $params = session_get_cookie_params();
  setcookie(session_name(), '', time() - 42000,
    $params['path'], $params['domain'], $params['secure'], $params['httponly']);
} //cookie + $_SESSION effacés => session détruite (normalement)
//mettre if(..) car on ne peut pas détruire une session qui n'existe pas
if (session_status() == PHP_SESSION_ACTIVE) { session_destroy(); }
session_start(); //redémarre une nouvelle session
?>
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Déconnexion</title>
  </head>
  <body>
    <p>Vous êtes déconnecté ... Au revoir !</p>
  </body>
</html>
  • session_id() : donne l’identifiant de session (identifiant PHP), ou une chaîne vide "" s’il n’y a pas de session.
  • ini_get() : Lit la valeur d’une option de configuration.
  • session_get_cookie_params() : Lit la configuration du cookie de session et retourne un tableau avec les clés ‘lifetime’ (durée de vie du cookie), ‘path’ (chemin ), ‘domain’ (le domaine), ‘secure’ (le cookie ne doit être envoyé que sur des connexions sécurisées), ‘httponly’ (le cookie n’est accessible que via le protocole HTTP).
  • setcookie(nom, valeur, expiration, chemin, domaine, secure, httponly) : définit un cookie qui sera envoyé (avec le reste des en-têtes).
  • session_status() : donne l’état de la session actuelle.

Expire après inactivité

On crée une variable de session $_SESSION['derniereAction'] contenant le timestamp de la dernière action.
Rappel : timestamp = le nombre de secondes écoulées depuis le 1er janvier 1970 (c’est un horodatage).

<?php
function renouvelleSessionInactive() { //si la dernière action date de 3600s (1h)
  if(isset($_SESSION['derniereAction']) && (time() - $_SESSION['derniereAction'] > 3600)) {
    if (session_id()) $message="<b>ancien sid = </b>".session_id();//facultatif : pour tester
    $_SESSION = array(); //détruit les variables de session
    if (ini_get('session.use_cookies')) { //et le cookie de session (expiration anti-datée)
      $params = session_get_cookie_params();
      setcookie(session_name(), '', time() - 42000,
            $params['path'], $params['domain'], $params['secure'], $params['httponly']);
    } //cookie + $_SESSION effacés => session détruite (normalement)
    //mettre if(..) car on ne peut pas détruire une session qui n'existe pas
    if (session_status() == PHP_SESSION_ACTIVE) { session_destroy(); }
    session_start(); //redémarre une nouvelle session
    if (session_id()) $message = $message."<b>nouveau sid = </b>".session_id(); //pour tester
  }
}

function raffraichirSession() {
  if (session_id()) session_commit(); //écrit les données puis ferme la session ouverte
  session_start();
  renouvelleSessionInactive();
  if (session_id()) $_SESSION['sid'] = session_id();
  $_SESSION['derniereAction'] = time(); //actualisation
}

raffraichirSession();
?>

<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Titre : page2.php</title>
  </head>
  <body>
    <?php echo "<p>$message</p>"; ?>
    <h2>[ Log-Out si inactivité ] Les variables de session</h2>
    <p><b>session identifiant (sid) :</b>  <?php echo $_SESSION['sid']; ?></p>
    <p><b>timestamp de la dernière action :</b> <?php echo $_SESSION['derniereAction']; ?></p>
  </body>
</html>

L’expiration après une inactivité n’est pas faite automatiquement par PHP car le session.gc_maxlifetime = 1440 de php.ini ne concerne que le garbage collector (gc) et celui-ci n’est appelé que dans 1% des cas !!

formulaire de login/logout

<?php
function test_input($data) {
  $data = trim($data);
  $data = stripslashes($data);
  $data = htmlspecialchars($data);
  return $data;
}

function raffraichirSession() {   //fonction à mettre sur toutes les pages
  if (session_id()) session_commit(); // écrit les données puis ferme la session ouverte
  session_start();
  $_SESSION['sid'] = session_id();
  $_SESSION['derniereAction'] = time();
}

raffraichirSession();
$identifiant = $mdp = $message = "";

if ($_SERVER['REQUEST_METHOD'] == "POST" ) {//Si réception d'un des 2 formulaires
  
  // --- POUR LE FORMULAIRE 'LOG IN'
  if ( isset($_POST['identifiant']) ) { //si c'est le formulaire 'Log In'
    $identifiant  = test_input($_POST["identifiant"]);
    $mdp          = test_input($_POST["mdp"]);  
    if (empty($identifiant)) $message = "Entrer un identifiant. "; 
    if (empty($mdp)) $message = $message."Entrer un mot de passe. ";

    if (empty($message)) { // s'il n'y a pas de message d'erreur identifiant/mdp
      $db = new mysqli('localhost', 'my_user', 'my_password', 'my_db');
      $sql = 'SELECT `mdp` FROM `membres` WHERE `identifiant` = ?';
      $stmt = $db->prepare($sql);
      $stmt->bind_param('s', $identifiant);
      $stmt->execute();
      $stmt->store_result();  //enregistre les propriétés des résultats
      $nbMembres = $stmt->num_rows;
      $mdp_hash  = NULL;
      $stmt->bind_result($mdp_hash);
      $stmt->fetch();
      $stmt->free_result();
      $stmt->close();
      $db->close;
      if ($nbMembres == 1) { //Dans la BDD, le mdp est haché (surtout pas en clair)
        if ( password_verify($mdp, $mdp_hash) ) {
          $message = $message."<b>ancien sid = </b> ".session_id(); //facultatif, c'est juste pour le test
          session_regenerate_id(true);
          $_SESSION = array(); //vide les variables de session
          $_SESSION['sid'] = session_id(); //le nouvel identifiant de session PHP
          $_SESSION['identifiant'] = $identifiant;
          $message = $message."<b>nouveau sid = </b>{$_SESSION['sid']}. ";//facultatif, que pour le test
        } else $message = "Mot de passe incorrect. ";
      } else $message = "Identifiant incorrect. ";
      $stmt->free_result();
      $stmt->close();
      $db->close;      
    }

  } else { // --- POUR LE FORMULAIRE 'LOG OUT'
    $_SESSION = array(); //détruit les variables de session
    if (ini_get('session.use_cookies')) { //et le cookie de session (expiration)
      $params = session_get_cookie_params();
      setcookie(session_name(), '', time() - 42000,
        $params['path'], $params['domain'], $params['secure'], $params['httponly']);
    } //cookie + $_SESSION effacés => session détruite (normalement)
    //mettre if(..) car on ne peut pas détruire une session qui n'existe pas
    if (session_status() == PHP_SESSION_ACTIVE) { session_destroy(); }
    session_start();
    $message = "Déconnexion réussie. "; 
  }
} // fin du traitement des 2 formulaires

raffraichirSession(); //car la situation a peut-être changé

if (!isset($_SESSION['identifiant'])) {//form LOG_IN ou LOG_OUT selon qu'on est déjà connecté
  $formulaire = '<form method="post" action="'.htmlspecialchars($_SERVER["PHP_SELF"]).'">'
    .'Identifiant : <input type="text" name="identifiant">. '
    .'Mot de passe : <input type="text" name="mdp"> '
    .'<input type="submit" name="log_in" value="Log In"><br />'
    .'<span class="message">'.$msgLogIn.' '.$message.'</span></form>';
} else {
  $formulaire = '<form method="post" action="'.htmlspecialchars($_SERVER["PHP_SELF"]).'">'
    ."Bienvenue {$_SESSION['identifiant']} "
    .'<span class="message">'.$message.'</span>'
    .'<input type="submit" name="log_out" value="Déconnexion"><br /></form>';
}
?>
<!DOCTYPE html>
<html lang="fr">
  <head>
    <meta charset="utf-8" />
    <title>Titre : page.php</title>
  </head>
  <body>
    <?php echo $formulaire; ?>
    <h1>Page quelconque</h1>
    <h2>les variables de session</h2>
    <p><b>session identifiant (sid) :</b>  <?php echo $_SESSION['sid']; ?></p>   
    <p><b>timestamp de la dernière action :</b> <?php echo $_SESSION['derniereAction']; ?></p>
  </body>
</html>
  • session_regenerate_id(); : laisse l’ancienne session intacte (équivalent à session_regenerate_id(false);).
  • session_regenerate_id(true); : détruit l’ancienne session
  • session_commit() équivalent à session_write_close() : Écrit les données de session et ferme la session.

PHP : cookies

Un cookie est un petit fichier enregistré sur l’ordinateur du client afin d’y stocker quelques informations (le login, pages web visités, articles du panier, … afin de récupérer ces info lors de la prochaine visite et adapter les encarts publicitaires).

créer un cookie

Le cookie contient au moins 3 choses :

  • le nom du cookie
  • la valeur du cookie
  • le timestamp de la date d’expiration du cookie

Donc le cookie ne contient qu’une seule information (et donc généralement, on crée plusieurs cookies).
<?php setcookie('pays', 'FRANCE', time() + 365*24*3600); ?> crée un cookie qui s’autodétruira dans 1 an.

avec le mode httpOnly

<?php setcookie('pays', 'FRANCE', time()+365*24*3600, null, null, false, true); ?>

  • setcookie(nom, valeur, expiration, chemin, domaine, secure, httponly) : définit un cookie qui sera envoyé avec le reste des en-têtes.
  • Il faut appeler setcookie()  AVANT le moindre code HTML (avant même le  <!doctype>, le moindre echo … ) c’est comme session_start().
  • Par sécurité, il est conseillé d’activer le mode httpOnly (pour éviter les failles XSS depuis le JavaScript). Ce mode httpOnly est le dernier paramètre, on le passe à true.
<?php
setcookie('pays', 'France', time()+365*24*3600, null, null, false, true); //crée un 1er cookie
setcookie('mail', 'truc@truc.fr', time()+365*24*3600, null, null, false, true); // 2e cookie
?>
<!DOCTYPE html>
<html>
  <head>
    <meta charset="utf-8" />
    <title>Titre de la page</title>
  </head>
  <body>
    <p>SIMPLE : Votre pays : <?php echo $_COOKIE['pays']; ?> (mais il est conseillé d'utiliser isset).</p>
    <p>MEILLEUR : <?php if (isset($_COOKIE['mail'])) echo "<p>Votre mail est $_COOKIE['mail'].</p>";?></p>
  </body>
</html>

modifier un cookie

Pour modifier un cookie déjà existant, on utilise  setcookie()  en gardant le même nom de cookie afin d’écraser l’ancien.
<?php setcookie('pays', 'Chine', time()+365*24*3600, null, null, false, true); ?>
NB : l’expiration remise à zéro (pour un an).

supprimer un cookie

On  on utilise  setcookie()  en mettant une date d’expiration antérieure à l’heure actuelle (et en gardant le même nom de cookie afin d’écraser l’ancien).
<?php setcookie('pays', 'Chine', time() - 300, null, null, false, true); ?>