Upload de fichiers via formulaire avec PHP
L'utilisation d'un langage côté serveur permet d'automatiser ou de simplifier bon nombre de tâches d'administration. L'upload (envoi) de fichiers fait partie de ces tâches qui peuvent être simplifiées. En effet, il est possible d'envoyer vos fichiers via un formulaire sur une page web puis d'effectuer le traitement de celui-ci grâce à PHP. Il ne sera donc plus nécessaire d'utiliser à chaque fois un client FTP.
Ceci pourra être très pratique dans le cas de l'administration d'une galerie photo par exemple, ou encore simplement pour illustrer vos publications.
Pré requis
Droits d'écriture
Pour que l'upload de fichiers puisse fonctionner correctement, il est impératif que le répertoire dans lequel on souhaitera placer les fichiers soit accessible en écriture. Sur certains systèmes, les dossiers sont toujours accessibles en écriture, c'est le cas de Windows (< Vista). En revanche, sur d'autres, tel que les systèmes dérivés d'UNIX, il est possible d'interdire l'écriture dans certains répertoires.
Ce réglage ne sera donc nécessaire que si le système de la machine qui héberge votre site utilise un mécanisme de gestions de droits sur les fichiers et répertoires.
Pour autoriser l'écriture, vous avez plusieurs possibilités. Soit en utilisant votre client FTP, auquel cas vous pourrez modifier les droits en accédant aux attributs ou aux propriétés de votre répertoire. Vous pouvez également modifier les droits de votre répertoire grâce à la fonction chmod() de PHP (reportez-vous à la documentation). Enfin, si vous avez accès directement au système de la machine (par SSH par exemple), vous pourrez modifier les droits très facilement grâce aux commandes appropriées (reportez vous à la documentation de votre système).
Configuration de php (php.ini)
Pour que l'envoie de fichiers fonctionne, PHP doit être configuré de la bonne manière. Ainsi certaine directives doivent avoir certaines valeurs. Vous pouvez modifier ces valeurs dans le fichier php.ini ou grâce à la fonction ini_set().
- file_uploads est la directive qui autorise ou interdit l'upload de fichier. Sa valeur doit donc être on (inverse de off).
- upload_tmp_dir correspond au répertoire dans lequel seront placé les fichiers temporairement uploadés. Ce répertoire doit lui aussi être accessible en écriture. Sur les systèmes UNIX et dérivés il s'agira souvent du répertoire /tmp/ accessible en écriture pour tous les utilisateurs du système.
- upload_max_filesize détermine la taille maximal de l'envoi d'un fichier. Il n'y a pas de valeur parfaite mais 2M est une valeur courante et satisfaisante dans la plupart des cas. Elle correspond à 2 méga octets.
- post_max_size correspond au volume maximum d'informations qui peuvent être envoyées via la méthode POST. Cette directive prime sur la précédente et doit logiquement avoir une valeur au moins égale. C'est pourquoi, là encore, 2M est une valeur satisfaisante.
Si vous rencontrez des difficultés dans l'upload de vos fichiers avec la méthodes que nous allons voir, c'est probablement à cause de votre configuration. SI vous ne pouvez pas y avoir accès (cas des hébergement mutualisés), utilisez la fonction phpinfo() qui vous donnera toutes les informations de configuration de php.
Le formulaire HTML
La première étape est de créer notre formulaire en HTML qui permettra l'envoie de fichiers au serveur. Pour cela, nous allons devoir préciser type de données qui sera envoyé via notre formulaire. Ce type est définit par défaut à application/x-www-form-urlencoded et ne permet pas l'envoie de fichiers. Nous allons le modifier au moyen de l'attribut enctype de la balise form et luis donner pour valeur multipart/form-data ce qui permettra l'envoie de n'importe quel type de données.
Enfin, pour permettre la sélection du fichier par le client, nous utiliserons un élément de formulaire de type file.
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd"> <html xmlns="http://www.w3.org/1999/xhtml" xml:lang="fr" lang="fr"> <head> <title>Formulaire d'upload</title> </head> <body> <form method="post" enctype="multipart/form-data" action="upload.php"> <p> <label>Sélectionnez un fichier : <input type="file" name="fichier" /> </label> </p> <p><input type="submit" value="Envoyer" /></p> </form> </body> </html>
Cet exemple de formulaire permet d'envoyer un seul fichier sur le serveur. Ce fichier sera placé dans un répertoire contenant des fichiers temporaires et, s'il ne fait pas l'objet d'un traitement supplémentaire, il sera supprimé à la fin de l'exécution de la requête. C'est pourquoi nous allons devoir faire intervenir PHP.
Le script PHP
Passons maintenant au script chargé du traitement approprié. Nous allons dans un premier temps en voir une version simple qui autorise tout types de fichiers.
Lorsque qu'un script reçoit les informations transmises par un formulaire, on utilise habituellement la variable globale $_POST pour les lire. Cependant, pour les élément de type file, on devra récupérer les informations concernant le fichiers via la variable $_FILES. Il s'agit d'un tableau associatif contenant différentes informations concernant les fichiers envoyés. La structure de ce tableau est la suivante :
Array
(
[nom_du_champ_file] => Array
(
[name] => "MaBelleImage.jpg" // Nom du fichier
[type] => "image/jpg" // type de fichier
[tmp_name] => "chemin_complet_du_fichier_uploadé" // Emplacement temporaire
[error] => 0 // Code d'erreur si erreur il y a
[size] => 1000 // Taille du fichier
)
)
A partir de ces informations, nous pourrons effectuer différents contrôles que nous verrons un peu plus tard. Pour le moment, nous allons simplement sauvegarder le fichier envoyé.
<?php // On définit notre répertoire cible $dstRep = './fichiers/'; // On vérifie qu'il est accessible en écriture if(!is_writable($dstRep)) die('Impossible d\'écrire dans le répertoire cible.'); // On vérifie d'abord que des données ont bien été envoyées if(!isset($_FILES['fichier'])) die('Aucune données'); // On finit par déplacer le fichier dans le répertoire cible move_uploaded_file($_FILES['fichier']['tmp_name'],$dstRep.$_FILES['fichier']['name']); ?>
Bien que ce script soit théoriquement fonctionnel, il est très imparfait. En effet, il ne contrôle pas le bon déroulement de l'envoie du fichier au moyen des codes d'erreur pouvant être indiqués dans le tableau $_FILES. En effet, la valeur de ce code nous indique le type d'erreur qui s'est produite durant l'envoie.
<?php // On définit notre répertoire cible $dstRep = './fichiers/'; // On vérifie qu'il est accessible en écriture if(!is_writable($dstRep)) die('Impossible d\'écrire dans le répertoire cible.'); // On vérifie d'abord que des données ont bien été envoyées if(!isset($_FILES['fichier'])) die('Aucune données'); // Vérification du code d'erreur switch($_FILES['fichier']['error']) { case 1: // UPLOAD_ERR_INI_SIZE die("Le fichier dépasse la limite autorisée par le serveur (fichier php.ini) !"); break; case 2: // UPLOAD_ERR_FORM_SIZE die("Le fichier dépasse la limite autorisée dans le formulaire HTML !"); break; case 3: // UPLOAD_ERR_PARTIAL die("L'envoi du fichier a été interrompu pendant le transfert !"); break; case 4: // UPLOAD_ERR_NO_FILE die("Le fichier que vous avez envoyé a une taille nulle !"); break; } // On finit par déplacer le fichier dans le répertoire cible move_uploaded_file($_FILES['fichier']['tmp_name'],$dstRep.$_FILES['fichier']['name']); ?>
Ainsi, ce script ne devrait pas lever d'erreur. A noter que d'autres types d'erreurs ont été introduit avec les nouvelles versions de PHP. Cependant, il reste assez incomplet. Nous allons maintenant lui ajouter quelques fonctionnalités supplémentaires. Et pour commencer, une indispensable, éviter l'écrasement des fichiers.
Éviter l'écrasement de fichier
Le problème que risque de poser le script que nous avons vu est l'écrasement de fichiers déjà existant. En effet, si vous envoyez deux fichiers portant le même nom, le premier fichier sera écrasé par le second. Pour remédier à cela, je vous propose une solution assez simple qui consiste à préfixer le nom du fichier d'un chiffre. Il existe d'autres solutions mais celle-ci est assez efficace et durable.
<?php // On définit notre répertoire cible $dstRep = './fichiers/'; // On vérifie qu'il est accessible en écriture if(!is_writable($dstRep)) die('Impossible d\'écrire dans le répertoire cible.'); // On vérifie d'abord que des données ont bien été envoyées if(!isset($_FILES['fichier'])) die('Aucune données'); // Vérification du code d'erreur switch($_FILES['fichier']['error']) { case 1: // UPLOAD_ERR_INI_SIZE die("Le fichier dépasse la limite autorisée par le serveur (fichier php.ini) !"); break; case 2: // UPLOAD_ERR_FORM_SIZE die("Le fichier dépasse la limite autorisée dans le formulaire HTML !"); break; case 3: // UPLOAD_ERR_PARTIAL die("L'envoi du fichier a été interrompu pendant le transfert !"); break; case 4: // UPLOAD_ERR_NO_FILE die("Le fichier que vous avez envoyé a une taille nulle !"); break; } // On vérifie si le fichier existe déjà dans le répertoire cible if(file_exists($dstRep.$_FILES['fichier']['name'])) { // On utilise une boucle pour incrémenter notre préfixe $i = 0; while(file_exists($dstRep.$i.'_'.$_FILES['fichier']['name']) ) $i++ // Arrivé ici, on a trouvé un nom disponible avec un préfixe numérique $cible = $dstRep.$i.'_'.$_FILES['fichier']['name']; } else { // Le fichier n'existe pas dans le répertoire cible, // Nous pouvons donc utiliser le nom original $cible = $dstRep.$_FILES['fichier']['name']; } // On finit par déplacer le fichier dans le répertoire cible move_uploaded_file($_FILES['fichier']['tmp_name'],$cible); ?>
Avec cette nouvelle version, les fichiers ne pourront pas être écrasés. Nous allons maintenant voir comment limiter l'envoie à certains types de fichiers.
Limiter les types de fichiers autorisés
Pour instaurer cette limitation, on pourrait éventuellement se baser sur l'information de type (Type mime) contenue dans le tableau $_FILES. Cependant, cette information est fournit par le client, le navigateur. La règle d'or en matière de développement web est de ne jamais avoir confiance dans les données transmises par un tiers inconnu. D'autre part, cette information pourra être un peut différente pour un même type de fichier et des navigateurs différents. Pour ces raisons, on ne tiendra pas compte de cette information.
Idéalement, il faudrait utiliser une méthode consistant à rechercher le type mime à partir du contenu du fichier. Il existe, pour cela la, fonction mime_content_type() mais elle est désormais déprécier au profit de Fileinfo.
Pour faire simple ici, nous baserons notre test sur l'extension du fichier. Pour que cette méthode soit suffisamment sûr, il est important que votre serveur soit correctement configuré pour ne pas exécuter de code côté serveur dans des fichiers ne portant pas une extension propre au langage. On a pu voir, par exemple, des serveurs autorisant l'exécution de code PHP dans fichiers portant l'extension .jpg, ce qui est un peu douteu...
<?php // On définit notre répertoire cible $dstRep = './fichiers/'; // On créé un tableau contenant les extensions autorisées $extOk = array('.jpg','.gif','.png'); // On vérifie qu'il est accessible en écriture if(!is_writable($dstRep)) die('Impossible d\'écrire dans le répertoire cible.'); // On vérifie d'abord que des données ont bien été envoyées if(!isset($_FILES['fichier'])) die('Aucune données'); // Vérification du code d'erreur switch($_FILES['fichier']['error']) { case 1: // UPLOAD_ERR_INI_SIZE die("Le fichier dépasse la limite autorisée par le serveur (fichier php.ini) !"); break; case 2: // UPLOAD_ERR_FORM_SIZE die("Le fichier dépasse la limite autorisée dans le formulaire HTML !"); break; case 3: // UPLOAD_ERR_PARTIAL die("L'envoi du fichier a été interrompu pendant le transfert !"); break; case 4: // UPLOAD_ERR_NO_FILE die("Le fichier que vous avez envoyé a une taille nulle !"); break; } // On lit l'extension du fichier $fileExt = substr($_FILES['fichier']['name'],-4,4); // On vérifie que l'extension est dans le tableau // des extensions autorisées if(in_array($fileExt,$extOk)) die("Type de fichier non autorisé."); // On vérifie si le fichier existe déjà dans le répertoire cible if(file_exists($dstRep.$_FILES['fichier']['name'])) { // On utilise une boucle pour incrémenter notre préfixe $i = 0; while(file_exists($dstRep.$i.'_'.$_FILES['fichier']['name']) ) $i++ // Arrivé ici, on a trouvé un nom disponible avec un préfixe numérique $cible = $dstRep.$i.'_'.$_FILES['fichier']['name']; } else { // Le fichier n'existe pas dans le répertoire cible, // Nous pouvons donc utiliser le nom original $cible = $dstRep.$_FILES['fichier']['name']; } // On finit par déplacer le fichier dans le répertoire cible move_uploaded_file($_FILES['fichier']['tmp_name'],$cible); ?>
Le système que nous venons de voir ne permet que de tester les extensions d'une longueur de trois caractère et nécessitera quelques modifications pour tester des extensions plus longue. Cependant, par convention, on se limite à 3 caractères pour l'extension d'un fichier.
Conclusion
Nous avons vu l'upload de fichiers avec PHP et quelques test pour aller un peu plus loin. Permettez moi un conseil, restez prudent avec l'envoie de fichiers car ceci peu ouvrir d'énormes failles sur vos sites. N'hésitez donc pas mettre en œuvre le maximum de test de sécurité sur les données envoyées.


