Développement d'applications Web avec LAMP

Document d'accompagnement pour le cours 420-KB9-LG

Téléversement de fichiers

Dernière mise à jour le 21 novembre 2022

Le formulaire

Pour téléverser un fichier, vous avez besoin d'un formulaire utilisant la méthode POST, ce qui signifie que les données (contenu du fichier) seront transmises dans le corps de la requête HTTP et non dans l'URL comme ce serait le cas avec une requête GET.

Champ de type file

Ce formulaire doit contenir un champ d'entrée de type file, ce qui fait apparaître une boîte de sélection de fichier :

<form action="traiter_fichier.php" method="post">
  Fichier : <input name="fichier" size="35" type="file">
</form>

Sortie (essayez pour voir!) :

Fichier :

Encodage multipart/form-data

Pour que le fichier sélectionné soit téléchargé vers le serveur (téléversé), vous devez absolument spécifier le type de codage des données (enctype) suivant : multipart/form-data.

<form action="traiter_fichier.php" method="post" enctype="multipart/form-data">
  Fichier : <input name="fichier" size="35" type="file">
</form>

L'attribut enctype de la balise form spécifie le type de codage utilisé par le navigateur pour envoyer les données au serveur (quand la valeur de l'attribut method est post).

Le tableau suivant indique les types de codage courants :

Type de codageDescription
application/x-www-form-urlencoded

Les données de formulaire organisées sous la forme de paires nom-valeur. Tous les caractères spéciaux sont encodés. Par exemple, les espaces deviennent des +.

Exemple :

Nom: Jean Narrache
Age: 33      

devient :

Nom=Jean+Narrache&Age=33      

Il s'agit là du format de codage standard (valeur par défaut avec la méthode POST).

multipart/form-data

Les données de formulaire sont codées sous la forme d'un message comportant une partie distincte pour chacun des champs. Nécessaire en présence de champs d'entrée de type file.

Exemple de formulaire :

<form action="http://patoche.org/televerser.php" method="post" enctype="multipart/form-data">
  <input type="text" name="texte" value="Le texte">
  <input type="file" name="fichier1">
  <input type="file" name="fichier2">
  <button type="submit">Submit</button>
</form>

Requête générée :

POST /televerser.php HTTP/1.1
Host: patoche.org
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux i686;
    rv:29.0) Gecko/20100101 Firefox/29.0
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Language: en-US,en;q=0.5
Accept-Encoding: gzip, deflate
Connection: keep-alive
Content-Type: multipart/form-data; boundary=--LALALALA
Content-Length: 5540

--LALALALA Content-Disposition: form-data; name="texte" Le texte
--LALALALA Content-Disposition: form-data; name="fichier1"; filename="foo.txt" Content-Type: text/plain Contenu de foo.txt
--LALALALA Content-Disposition: form-data; name="fichier2"; filename="bqr.html" Content-Type: text/html <!DOCTYPE html><title>Contenu de bar.htmlt</title>
--LALALALA--
text/plain

Les données de formulaire sont codées en texte brut, sans aucun contrôle ni caractères de mise en forme

Rarement utilisé, sauf pour des fins de teste et de débogage.

Maintenant rassurez-vous : vous n'avez pas besoin de maîtriser ces différents types d'encodages! Retenez seulement que pour le téléversement de fichiers vous devez utiliser multipart/form-data.

MAX_FILE_SIZE

Un champ caché nommé MAX_FILE_SIZE doit précéder le champ input de type file.

Il spécifie la taille maximale en octets d'un fichier pouvant être téléchargé vers le script.

Attention : cette valeur peut être contournée et il est nécessaire de vérifier la taille du fichier transmis une fois celui-ci rendu sur le serveur.

<form action="traiter_fichier.php" method="post" enctype="multipart/form-data">
  <input type="hidden" name="MAX_FILE_SIZE" value="50000">
  Fichier : <input name="fichier" size="35" type="file">
</form>

Nom du fichier

Enfin, faites attention au nom de l'élément input de type text qui sert à saisir le nom, car c'est lui qui déterminera l'indice dans le tableau $_FILES qui sera utilisé par le script sur le serveur (voir plus loin).

Version complète du formulaire :

<form action="traiter_fichier.php" method="post" enctype="multipart/form-data">
  <input type="hidden" name="MAX_FILE_SIZE" value="50000">
  Fichier : <input name="fichier" size="35" type="file">
  <input type="submit" value="Envoyer le fichier">
</form>

Traitement des fichiers téléversés

À sa réception sur le serveur, un fichier téléversé est stocké sous un nom temporaire dans un répertoire spécifique (ex : "/tmp/phprEXcl3").

Les informations qui se rattachent aux fichiers téléversés sont stockées côté serveur dans un tableau à deux dimensions appelé $_FILES.

Exemple :

// informations sur le fichier envoyé via le champ d'entrée "fichier"
$_FILES['fichier']['name']
$_FILES['fichier']['type']
...
// informations sur le fichier envoyé via le champ d'entrée "foobar"
$_FILES['foobar']['name']
...

En supposant que le fichier est envoyé via un champ d'entrée appelé "fichier", l'élément $_FILES['fichier']['name'] contiendra par exemple le "vrai" nom du fichier.

L'ensemble des informations disponibles est représenté dans le tableau suivant :

VariableContenu
$_FILES['fichier']['name']Nom du fichier
$_FILES['fichier']['type']Type (par exemple text/plain pour un fichier texte ordinaire)
$_FILES['fichier'] ['tmp_name']Nom du fichier temporaire (créé dans un répertoire spécifique du système)
$_FILES['fichier']['error']Numéro d'erreur (pas toujours fiable)
$_FILES['fichier']['size']Taille du fichier en octets

Dans l'exemple suivant, on utilise la fonction is_uploaded_file() pour déterminer si le fichier spécifié est bel et bien un fichier téléversé.

Même si ce n'est pas obligatoire, c'était une pratique répandue dans les scripts que de s'assurer qu'un fichier qu'on déplace d'un répertoire "système" vers un répertoire public est bien un fichier téléversé. Cela date d'une époque où PHP était un langage globalement moins "sécuritaire" qu'aujourd'hui.

On s'entend pour dire que cela n'est plus nécessaire maintenant.

Le bout de code suivant affiche le contenu du fichier reçu, à condition qu'il s'agisse véritablement d'un fichier téléversé :

// si le fichier contient du HTML ou du texte
if (($_FILES['fichier']['type'] == "text/html") ||
        ($_FILES['fichier']['type'] == "text/plain")) {
        
    // s'il s'agit vraiment d'un fichier téléversé (plus nécessaire
    // mais encore utilisé par habitude)
    if (is_uploaded_file($_FILES['fichier']['tmp_name'])) {
    
        // on envoie le contenu dans la sortie standard (le navigateur)
        readfile($_FILES['fichier']['tmp_name']);
    }
} else {
    echo "Le fichier ne peut être affiché";
}

Pour stocker le fichier dans un répertoire de l'application, il faut utiliser la fonction move_uploaded_file().

Remarque : cette fonction vérifie également qu'il s'agit bien d'un fichier téléversé et il est encore moins nécessaire d'utiliser ici la fonction is_uploaded_file().

Le bout de code suivant déplace le fichier téléchargé dans le répertoire "fichiers" de l'application :

// répertoire où l'application conserve les fichiers téléversés
$rep = 'fichiers/';

// nouveau chemin du fichier à conserver
$fich = $rep . basename($_FILES['fichier']['name']);

if (move_uploaded_file($_FILES['fichier']['tmp_name'], $fich)) {
    echo "Le fichier est valide et a été déplacé dans " . $fich;
} else {
    echo "Problème lors du déplacement";
}

Remarque : la fonction basename() extrait le nom du fichier à partir du nom complet (enlève le chemin si existant).

Pour débogage

Le code suivant affiche toutes les données disponibles sur le fichier téléversé. N'hésitez pas à l'utiliser pour déboguer.

<?php
  $nom    = $_FILES['fichier']['name'];
  $taille = $_FILES['fichier']['size'];
  $temp   = $_FILES['fichier']['tmp_name'];
  $type   = $_FILES['fichier']['type'];
  $erreur = $_FILES['fichier']['error'];

  echo "<pre>";
  echo "Nom d'origine          : $nom\n";
  echo "Taille                 : $taille octets\n";
  echo "Emplacement temporaire : $temp\n";
  echo "Type MIME              : $type\n";
  echo "Code d'erreur          : $erreur\n";
  echo "</pre>";
?>

Remarque : le code d'erreur retourné n'est malheureusement pas toujours fiable.

Le code suivant affiche l'état du téléchargement :

<?php
  if ($erreur = $_FILES['fichier']['error']) {
      echo "Une erreur s'est produite : ";
      
      switch ($erreur) {
          case UPLOAD_ERR_INI_SIZE:
              echo "le fichier est plus gros que le maximum autorisé";
              break;
          case UPLOAD_ERR_FORM_SIZE:
              echo "le fichier est plus gros qu'indiqué dans le formulaire";
              break;
          case UPLOAD_ERR_PARTIAL:
              echo "le fichier n'a été que partiellement téléversé";
              break;
          case UPLOAD_ERR_NO_FILE:
              echo "aucun fichier n'a été téléversé";
      }
  } else {
      echo "Fichier correctement téléversé";
  }
?>

Problèmes courants

Parmi les problèmes rencontrés souvent avec le téléversement, il y a d'abord les valeurs limites imposées par la configuration PHP.

Les valeurs qui peuvent affecter le téléversement, telles que définies dans le fichier php.ini sont :

On peut vérifier ces valeurs à l'aide de la fonction phpinfo().

Les valeurs typiques de ces constantes sont :

upload_max_filesize =   2M
post_max_size       =   8M
memory_limit        = 128M
max_file_uploads    =   20
max_execution_time  =   30
max_input_time      =   60

Note personnelle : par le passé j'ai dû jouer avec la valeur des trois premières constantes pour autoriser le téléversement de gros fichiers.

Consultez la documentation pour en savoir plus!

Un autre problème fréquent consiste à oublier d'ouvrir à tous les internautes le répertoire cible de la fonction move_uploaded_file().

Sous Linux, on y parvient avec la commande suivante :

$ sudo chgrp www-data nom_du_répertoire

Enfin, si notre distribution Linux utilise le module de sécurité SELinux, il faudra changer le contexte de ce répertoire :

sudo chcon -R -t httpd_sys_rw_content_t répertoire

Et voilà! Bon téléversement!