Note : le scipt donné en exemple est fonctionnel et compatible PHP >= 5.2. Il est cependant plus destiné à illustrer le tuto qu'à une utilisation directe. Si vous souhaité un script prêt à l'emploi (avec de nombreuses autres fonctionnalités en option) utilisez plutôt la classe d'upload de mon second message.
1 / Partie HTML, le formulaire
L'entête du formulaire doit intégrer la mention enctype = "multipart/form-data", par ailleurs la méthode d'envoi des données est de type post.
Un champ invisible de type "hidden" ex : name = "chargement", sert de témoin d'envoi du formulaire. On pourrait faire plus simple mais cela nous servira par la suite pour intégrer plusieurs formulaires dans une même page.
Le champ invisible de type "hidden" : name = "MAX_FILE_SIZE" value = "valeur en octets", est facultatif. Il peut être utilisé pour limiter la taille d'envoi du fichier (à une valeur inférieure à la valeur autorisée dans le paramètre de configuration du serveur "upload_max_filesize"). Il doit précéder le champ de type file pour être pris en compte.
Le champ de type "file" est le champ de téléchargement des fichiers. Si l'on veut plusieurs champs successifs on nomme ce champ sous forme d'un tableau ex : name = toto[], mais bien entendu dans ce cas il faudrait un script PHP adapté (disponible dans la classe en lien dans mon second message).
2 / Partie PHP, le traitement
A/ Les champs de type "file"
Un champ de type "file" renvoie un tableau accessible dans la variable globale $_FILES. Plus d'info sur ce lien. Les trois éléments du tableau utilisés dans ce script sont :
- L'index "name" qui indique le nom original du fichier, tel que sur la machine du client web.
- L'index "tmp_name" qui indique le nom du fichier temporaire qui est téléchargé sur le serveur avant son déplacement vers l'adresse finale.
- L'index "error" qui renvoie le code d'erreur associé au téléchargement de fichier.
Quand on télécharge un fichier, ce fichier est donc téléchargé sur le serveur avec un nom temporaire, puis on fait les traitements/vérifications souhaitées et ensuite on déplace ce fichier temporaire vers l'adresse de destination voulue avec la fonction "move_uploaded_file".
B/ Limitations et gestion des erreurs
- Il existe une limitation directe de la taille du fichier qui est le paramètre de configuration du serveur "upload_max_filesize".
- La limitation de la taille maximale d'envoi du formulaire post (total de tous les champs input) est le paramètre "post_max_size" qui est généralement supérieure à "upload_max_filesize".
Si le fichier téléchargé a une taille supérieure à "upload_max_filesize" mais inférieure ou égale à "post_max_size" on peut récupérer le message d'erreur correspondant dans l'index "error" du tableau $_FILES évoqué plus haut.
Si un, ou l'ensemble des fichiers téléchargés dépasse la valeur "post_max_size" les tableaux $_POST et $_FILES seront vides.
Pour récupérer le message d'erreur correspondant on envoie donc une variable $_GET qui nous servira de test, en même temps que le formulaire (dans l'attribut "action" de la balise "form"). Si cette variable $_GET existe mais que la variable $_POST témoin d'envoi du formulaire n'existe pas, on est donc dans ce cas de figure : "post_max_size" est dépassé et l'on envoie le message d'erreur approprié.
C/ Fonctionnement par défaut
Par défaut, si on télécharge un fichier dont le nom est identique à un nom de fichier déjà existant sur le serveur, le fichier du serveur est écrasé et remplacé par ce nouveau fichier. Ce n'est pas toujours ce que l'on souhaite...
D/ La fonction pratique
Ce script de téléchargement possède la particularité de faire appel à une fonction de vérification et de renommage du fichier téléchargé en mode incrémentiel si celui-ci possède un nom identique à un fichier déjà existant sur le serveur. On évite donc l'écrasement éventuel d'un fichier existant tout en permettant le téléchargement du nouveau fichier après son renommage automatique.
E / Enregistrement du résultat
Une variable de session est utilisée pour enregistrer le résultat du téléchargement et les éventuels messages d'erreurs (extension du fichier non valide etc.). L'emploi d'une variable de session permet de conserver le contenu du message après le rechargement de la page qui est effectué en fin de script pour éviter un double post au cas où l'utilisateur ferait un rafraichissement de la page.
F / Y'a plus qu'à...
Le code est documenté au fil de l'eau. N'hésitez pas à cliquer sur les fonctions natives utilisées pour obtenir plus d'information sur leur fonctionnement dans le manuel.
<?php
header('Content-type: text/html; charset=UTF-8');
session_start();
/* Fonction de renommage : si fichier.ext existe, renomme en fichier_1.ext ; si fichier_1.ext existe, renomme en fichier_2.ext etc.. */
function Rename_fich($adresse_fichier, $incr = true)
{
if (is_file($adresse_fichier))
{
$info = pathinfo($adresse_fichier);
$extension = trim($info['extension']) != '' ? '.'.$info['extension'] : null;
$dossier = $info['dirname'];
$filename = $info['filename'];
if (trim($incr) != false)
{
$file = addcslashes($filename,'.');
$ext = isset($extension) ? addcslashes($extension,'.') : null;
$match = '#^'.$file.'_[0-9]+'.$ext.'$#';
$tab_identique = array();
$files = new RegexIterator(new DirectoryIterator($dossier),$match);
foreach ($files as $fileinfo) $tab_identique[] = $fileinfo->getFilename();
natsort($tab_identique);
$dernier = array_pop($tab_identique);
unset($tab_identique);
$dernier = isset($dernier)? basename($dernier,$extension) : '';
$file = preg_replace_callback('#([0-9]+$)#', create_function('$matches','return $matches[1]+1;'), $dernier, '1', $count);
$filename = !empty($count)? $file : $filename.'_1';
}
else
{
$filename .= '_'.uniqid();
}
$filename = isset($extension) ? $filename.$extension : $filename;
$adresse = $dossier.'/'.$filename;
if (!is_file($adresse)) return $adresse;
else
return Rename_fich($adresse_fichier,$incr);
}
else
{
return $adresse_fichier;
}
}
/* Fonction qui interromp le script et recharge la page */
function Reload_page ()
{
header("Location:".$_SERVER['PHP_SELF']);
exit;
}
/* Fonction permettant de convertir une chaine de caractère en octets ex : "1K" est converti en "1024" (car 1 kilo octet = 1024 octets) */
function Return_Octets($val)
{
$val = trim($val);
$last = strtolower(substr($val,-1));
switch($last)
{
case 'g': $val *= 1024;
case 'm': $val *= 1024;
case 'k': $val *= 1024;
}
return $val;
}
/* Si le fichier dépasse la taille maximum du POST autorisée par le serveur - correspond à la valeur ini_get('post_max_size') - les tableaux $_POST et $_FILES seront vides. Néanmoins la variable GET['form_file'] envoyée en même temps que le formulaire est récupérée. Donc si la première valeur n'existe pas mais que la seconde existe, nous sommes dans ce cas de figure. */
if(!isset($_POST['chargement']) && isset($_GET['form_file']))
{
$_SESSION['resultat_telecharg'] = 'Fichier non téléchargé. Le fichier dépasse la taille maximum du POST autorisée par le serveur.';
Reload_page ();
}
/* Téléchargement du fichier vers le serveur si les variables du formulaire sont transmises */
if (isset($_POST['chargement'],$_FILES['userfile']['name']) && trim($_FILES['userfile']['name']) != '')
{
/* Répertoire de destination par rapport à la racine du site, exemple PHOTO */
$repertoire = 'PHOTO';
/* Tableau des extensions de fichier autorisées */
$extensions_ok = array('png','jpg','jpeg','gif');
/* Initialisation de la variable affichant le message de résultat du téléchargement */
$_SESSION['resultat_telecharg'] = null;
/* Gestion des erreurs c.f. [url=http://www.php.net/manual/fr/features.file-upload.errors.php]ce lien[/url] */
switch ($_FILES['userfile']['error'])
{
case "1" : $_SESSION['resultat_telecharg'] = "Le fichier excède la taille de fichiers autorisée par le serveur";break;
case "2" : $_SESSION['resultat_telecharg'] = "Le fichier excède la taille de fichiers autorisée dans le formulaire";break;
case "3" :
case "4" :
case "6" :
case "7" :
case "8" : $_SESSION['resultat_telecharg'] = "Fichier non téléchargé. Problème lors du téléchargement";break;
}
/* Si le fichier n'est correctement chargé on sort du script */
if ($_FILES['userfile']['error'] !== 0) Reload_page ();
/* Adresse racine du serveur */
$adresse = (substr($_SERVER['DOCUMENT_ROOT'],-1) == '/')? $_SERVER['DOCUMENT_ROOT'] : $_SERVER['DOCUMENT_ROOT'].'/' ;
/* Adresse du répertoire de destination */
$destination = $adresse.$repertoire.'/';
/* Vérification de la validité du répertoire, si non valide (signifié par "!is_dir") on sort du script en ayant enregistré un message d'erreur */
if (!is_dir($destination))
{
$_SESSION['resultat_telecharg'] = 'Chemin du dossier non valide';
Reload_page ();
}
/* Nom du fichier téléchargé */
$localfile = $_FILES['userfile']['name'];
/* Trouve l'extension du fichier et la met en minuscules */
$extension = strtolower(substr($localfile,strrpos($localfile, ".")+1));
/* Vérification que l'extension est contenue dans le tableau des extensions autorisées. Dans le cas contraire (signifié par "!in_array") on sort du script en ayant enregistré un message d'erreur. */
if (!in_array($extension,$extensions_ok))
{
/*Transforme le tableau en chaine de caractères séparés par une virgule suivie d'un espace pour affichage dans le message d'erreur */
$extensions_autorisees = implode(', ',$extensions_ok);
/* Message d'erreur d'extension du fichier */
$_SESSION['resultat_telecharg'] = 'Extension du fichier non autorisée. Extensions autorisées : '.$extensions_autorisees.'.';
Reload_page ();
}
/* Remplacement des caractères accentués par leur équivalent */
$nom_fichier = iconv('UTF-8', 'ASCII//TRANSLIT', $localfile);
/* Nettoyage des caractères non valides (reste les caractères alphanumériques et '.', '_', '-') */
$nom_fichier = preg_replace('#[^.a-z0-9_-]+#i', '', $nom_fichier);
/* Si après traitement le résultat est une chaine vide, on renregistre un message d'erreur et on sort du script */
if (trim($nom_fichier) == '')
{
$_SESSION['resultat_telecharg'] = 'Nom de fichier non valide. Veuillez renommer votre fichier avant le téléchargement';
Reload_page ();
}
/* Adresse du fichier de destination */
$adresse_fichier = $destination.$nom_fichier;
/* Application de la fonction "Rename_fich" pour vérifier l'existence d'un fichier de même nom dans le répertoire et si oui, renommage du fichier de téléchargement */
$adresse_fichier = Rename_fich($adresse_fichier);
/* Si chargement du fichier - avec le nom temporaire créé par le serveur - vers l'adresse de destination */
if(@move_uploaded_file($_FILES['userfile']['tmp_name'], $adresse_fichier))
{
/* Nom du fichier final */
$nom_fichier = basename($adresse_fichier);
/* Préparation des messages de confirmation */
$resultat1 = 'Le fichier "'.$localfile.'" a été téléchargé sur le serveur';
$resultat2 = 'Le fichier "'.$localfile.'" renommé "'.$nom_fichier.'" a été téléchargé sur le serveur';
/* Création du message de confirmation */
$_SESSION['resultat_telecharg'] = $localfile === $nom_fichier ? $resultat1 : $resultat2;
}
else
{
/* Le fichier n'a pas été téléchargé on sort du script en ayant enregistré un message d'erreur. */
$_SESSION['resultat_telecharg'] = 'Fichier non téléchargé. Problème lors du téléchargement';
Reload_page ();
}
/* Recharge la page pour éviter un nouveau téléchargement en cas de rafraichissement de la page */
Reload_page ();
}
?>
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
<title>Exemple téléchargement avec renommage des fichiers si déjà existants</title>
<script type="text/javascript">
function Verif_attente(id_file,id_attente)
{
var id_file = document.getElementById(id_file);
if (id_file && id_file.value == '') return false;
var id_attente = document.getElementById(id_attente);
if (id_attente)
{
/* Nettoyage de l'élément cible */
var nb_noeuds = id_attente.childNodes.length;
for (var i = 0; i < nb_noeuds; i++)
{
id_attente.removeChild(id_attente.firstChild);
}
var texte = 'Patientez...';
/* Création du noeud texte */
var noeud_texte = document.createTextNode(texte);
/* Insertion du noeud texte */
id_attente.appendChild(noeud_texte);
}
}
</script>
</head>
<body>
<div id = "message_tele"><?php if (isset($_SESSION['resultat_telecharg'])) {echo htmlspecialchars($_SESSION['resultat_telecharg']); unset($_SESSION['resultat_telecharg']);}?>
</div>
<form enctype = "multipart/form-data" action = "<?php echo htmlspecialchars($_SERVER['PHP_SELF']).'?form_file=1'?>" method = "post" onsubmit = "Verif_attente('userfile','message_tele')">
<p>
<input type = "hidden" name = "chargement" value = "1" />
<!-- Vous pouvez spécifier dans l'input "MAX_FILE_SIZE" la taille maximum que vous autorisez avec la fonction Return_Octets qui convertira votre valeur en octets.
exemples : Return_Octets('2M') pour indiquer 2 méga octets, Return_Octets('500K') pour 500 kilos octets, Return_Octets('1G') pour un giga octet ...
Cette valeur doit être inférieure ou égale à ini_get('upload_max_filesize') définie par le serveur -->
<input type = "hidden" name = "MAX_FILE_SIZE" value = "<?php echo Return_Octets(ini_get('upload_max_filesize'))?>" />
<input id = "userfile" name = "userfile" type = "file" size = "70" />
<input type = "submit" value = "Envoyez" style = "margin-left:5px" />
</p>
</form>
<p>Taille maximum par fichier autorisée par le serveur = <?php echo ini_get('upload_max_filesize').'o'?></p>
</body>
</html>
Notes : - Pour utiliser ce script vous devrez rentrer votre valeur pour le nom du répertoire de destination - dans mon exemple $repertoire = 'PHOTO' - et compléter/modifier si besoin le tableau "$extensions_ok" des extensions de fichiers autorisées.
- Le chemin du répertoire est construit à partir de la racine du serveur ...www suivi du nom du répertoire.
Si vous faites des essais sur un serveur local, il faudra indiquer le nom du site puis du répertoire dans la variable $repertoire pour indiquer le bon emplacement. Par exemple, avec plusieurs sites installés en localhost sur le répertoire www dont un se nomme "monsite1", pour faire des essais avec ce site indiquez : $repertoire = 'monsite1/PHOTO' pour indiquer le répertoire PHOTO.
Transformation du script procédural en mode objet ?
Tel quel, le script ci-dessus est fonctionnel. Cependant la modification des paramètres :
- répertoire de destination
- tableau d'extension des fichiers
- nom des champs du formulaire utilisés dans le code php
nécessite de remettre le nez dans le code.
Idem si l'on ne souhaite pas renommer les fichiers mais les écraser. Son utilisation n'est donc pas aisée dès que l'on doit modifier un de ces paramètres.
De plus le script n'est pas prévu pour proposer plusieurs formulaires de téléchargement dans une même page, non plus pour plusieurs champs dans un même formulaire.
Pour ce faire la création d'une classe objet va nous faciliter grandement la tâche...
Suivre ce lien pour la suite (dossier de test pré configuré prêt à l'emploi)
Et si vous avez besoin d'aller plus loin
- pour télécharger des gros fichiers et ne plus être limité par les configurations serveur "post_max_size" et "upload_max_filesize'
- avoir des retours d'informations en temps réel
- pouvoir reprendre un téléchargement interrompu
- permettre la prévisualisation des images avant leur téléchargement
- etc...
Vous pouvez utiliser cette solution qui propose un exemple de boite d'upload complète et customisée supportant le drag and drop.
(cette dernière solution n'est compatible qu'à partir d' IE 10)